Repository: ThreeMammals/Ocelot Branch: develop Commit: 9acc27195c18 Files: 1039 Total size: 4.0 MB Directory structure: gitextract_iih378bx/ ├── .config/ │ └── dotnet-tools.json ├── .dockerignore ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── steps/ │ │ ├── check-dotnet.sh │ │ ├── macos.add-dns-records.sh │ │ ├── macos.install-certificate.sh │ │ ├── prepare-coveralls.sh │ │ ├── ubuntu.add-dns-records.sh │ │ ├── ubuntu.install-certificate.sh │ │ ├── windows.add-dns-records.ps1 │ │ └── windows.install-certificate.ps1 │ └── workflows/ │ ├── develop.yml │ ├── pr-closed.yml │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .readthedocs.yaml ├── GitVersion.yml ├── LICENSE.md ├── Ocelot.Samples.sln ├── Ocelot.Samples.slnx ├── Ocelot.sln ├── Ocelot.slnx ├── README.md ├── ReleaseNotes.md ├── build.cake ├── codeanalysis.ruleset ├── codecov.yml ├── coverlet.runsettings ├── docker/ │ ├── Dockerfile.base │ ├── Dockerfile.build │ ├── Dockerfile.release │ ├── Dockerfile.windows │ ├── README.md │ ├── build-windows.sh │ ├── build.sh │ └── outdated/ │ ├── Dockerfile.8.21.0.base │ └── Dockerfile.8.23.2.base ├── docs/ │ ├── Makefile │ ├── _static/ │ │ └── overrides.css │ ├── building/ │ │ ├── building.rst │ │ ├── devprocess.rst │ │ └── releaseprocess.rst │ ├── conf.py │ ├── features/ │ │ ├── administration.rst │ │ ├── aggregation.rst │ │ ├── authentication.rst │ │ ├── authorization.rst │ │ ├── caching.rst │ │ ├── claimstransformation.rst │ │ ├── configuration.rst │ │ ├── delegatinghandlers.rst │ │ ├── dependencyinjection.rst │ │ ├── errorcodes.rst │ │ ├── graphql.rst │ │ ├── headerstransformation.rst │ │ ├── kubernetes.rst │ │ ├── loadbalancer.rst │ │ ├── logging.rst │ │ ├── metadata.rst │ │ ├── methodtransformation.rst │ │ ├── middlewareinjection.rst │ │ ├── qualityofservice.rst │ │ ├── ratelimiting.rst │ │ ├── routing.rst │ │ ├── servicediscovery.rst │ │ ├── servicefabric.rst │ │ ├── tracing.rst │ │ └── websockets.rst │ ├── index.rst │ ├── introduction/ │ │ ├── bigpicture.rst │ │ ├── gettingstarted.rst │ │ ├── gotchas.rst │ │ └── notsupported.rst │ ├── make.bat │ ├── make.ps1 │ ├── make.sh │ ├── readme.md │ ├── releasenotes.rst │ └── requirements.txt ├── postman/ │ └── ocelot.postman_collection.json ├── samples/ │ ├── Basic/ │ │ ├── API.http │ │ ├── Ocelot.Samples.Basic.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── ocelot.json │ │ └── packages.lock.json │ ├── Configuration/ │ │ ├── API.http │ │ ├── Ocelot.Samples.Configuration.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── ocelot-configuration/ │ │ │ ├── ocelot.docs.json │ │ │ ├── ocelot.global.json │ │ │ ├── ocelot.posts.json │ │ │ └── ocelot.weather.json │ │ └── packages.lock.json │ ├── Eureka/ │ │ ├── ApiGateway/ │ │ │ ├── Ocelot.Samples.Eureka.ApiGateway.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.json │ │ │ ├── ocelot.json │ │ │ └── packages.lock.json │ │ ├── DownstreamService/ │ │ │ ├── Controllers/ │ │ │ │ └── CategoryController.cs │ │ │ ├── Ocelot.Samples.Eureka.DownstreamService.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── packages.lock.json │ │ ├── OcelotEureka.sln │ │ └── README.md │ ├── GraphQL/ │ │ ├── GraphQlDelegatingHandler.cs │ │ ├── Models/ │ │ │ ├── Hero.cs │ │ │ └── Query.cs │ │ ├── Ocelot.Samples.GraphQL.csproj │ │ ├── OcelotGraphQL.sln │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── ocelot.json │ │ └── packages.lock.json │ ├── Kubernetes/ │ │ ├── .dockerignore │ │ ├── ApiGateway/ │ │ │ ├── Dockerfile │ │ │ ├── Ocelot.Samples.Kubernetes.ApiGateway.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ ├── ocelot.json │ │ │ └── packages.lock.json │ │ ├── Dockerfile │ │ ├── DownstreamService/ │ │ │ ├── Controllers/ │ │ │ │ ├── ValuesController.cs │ │ │ │ └── WeatherForecastController.cs │ │ │ ├── Dockerfile │ │ │ ├── Models/ │ │ │ │ └── WeatherForecast.cs │ │ │ ├── Ocelot.Samples.Kubernetes.DownstreamService.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── packages.lock.json │ │ └── OcelotKube.sln │ ├── Metadata/ │ │ ├── API.http │ │ ├── MetadataResponder.cs │ │ ├── Models/ │ │ │ ├── PostsPlugin2.cs │ │ │ ├── TestDeflateResponse.cs │ │ │ ├── TestGZipResponse.cs │ │ │ ├── WeatherCurrent.cs │ │ │ ├── WeatherCurrentCondition.cs │ │ │ ├── WeatherLocation.cs │ │ │ └── WeatherResponse.cs │ │ ├── MyMiddlewares.cs │ │ ├── Ocelot.Samples.Metadata.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── ocelot.json │ │ └── packages.lock.json │ ├── OpenTracing/ │ │ ├── Ocelot.Samples.OpenTracing.csproj │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── ocelot.json │ │ └── packages.lock.json │ ├── ServiceDiscovery/ │ │ ├── .dockerignore │ │ ├── ApiGateway/ │ │ │ ├── Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── ServiceDiscovery/ │ │ │ │ ├── MyServiceDiscoveryProvider.cs │ │ │ │ └── MyServiceDiscoveryProviderFactory.cs │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ ├── ocelot.json │ │ │ └── packages.lock.json │ │ ├── DownstreamService/ │ │ │ ├── Controllers/ │ │ │ │ ├── CategoriesController.cs │ │ │ │ ├── HealthController.cs │ │ │ │ └── WeatherForecastController.cs │ │ │ ├── Models/ │ │ │ │ ├── HealthResult.cs │ │ │ │ ├── MicroserviceResult.cs │ │ │ │ ├── ReadyResult.cs │ │ │ │ └── WeatherForecast.cs │ │ │ ├── Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ └── packages.lock.json │ │ ├── Ocelot.Samples.ServiceDiscovery.sln │ │ └── README.md │ ├── ServiceFabric/ │ │ ├── .gitignore │ │ ├── ApiGateway/ │ │ │ ├── Ocelot.Samples.ServiceFabric.ApiGateway.csproj │ │ │ ├── OcelotApplicationApiGateway.cs │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── ServiceEventListener.cs │ │ │ ├── ServiceEventSource.cs │ │ │ ├── WebCommunicationListener.cs │ │ │ ├── appsettings.Development.json │ │ │ ├── appsettings.json │ │ │ ├── ocelot.json │ │ │ └── packages.lock.json │ │ ├── CONTRIBUTING.md │ │ ├── DownstreamService/ │ │ │ ├── ApiGateway.cs │ │ │ ├── Controllers/ │ │ │ │ └── ValuesController.cs │ │ │ ├── Ocelot.Samples.ServiceFabric.DownstreamService.csproj │ │ │ ├── Program.cs │ │ │ ├── Properties/ │ │ │ │ └── launchSettings.json │ │ │ ├── ServiceEventSource.cs │ │ │ └── packages.lock.json │ │ ├── LICENSE.md │ │ ├── Ocelot.Samples.ServiceFabric.sln │ │ ├── OcelotApplication/ │ │ │ ├── ApplicationManifest.xml │ │ │ ├── OcelotApplicationApiGatewayPkg/ │ │ │ │ ├── Code/ │ │ │ │ │ ├── entryPoint.cmd │ │ │ │ │ └── entryPoint.sh │ │ │ │ ├── Config/ │ │ │ │ │ ├── Settings.xml │ │ │ │ │ └── _readme.txt │ │ │ │ ├── Data/ │ │ │ │ │ └── _readme.txt │ │ │ │ ├── ServiceManifest-Linux.xml │ │ │ │ ├── ServiceManifest-Windows.xml │ │ │ │ └── ServiceManifest.xml │ │ │ └── OcelotApplicationServicePkg/ │ │ │ ├── Code/ │ │ │ │ ├── entryPoint.cmd │ │ │ │ └── entryPoint.sh │ │ │ ├── Config/ │ │ │ │ ├── Settings.xml │ │ │ │ └── _readme.txt │ │ │ ├── Data/ │ │ │ │ └── _readme.txt │ │ │ ├── ServiceManifest-Linux.xml │ │ │ ├── ServiceManifest-Windows.xml │ │ │ └── ServiceManifest.xml │ │ ├── README.md │ │ ├── build.bat │ │ ├── build.sh │ │ ├── dotnet-include.sh │ │ ├── install.ps1 │ │ ├── install.sh │ │ ├── uninstall.ps1 │ │ └── uninstall.sh │ └── Web/ │ ├── DownstreamHostBuilder.cs │ ├── Ocelot.Samples.Web.csproj │ ├── OcelotHostBuilder.cs │ ├── Properties/ │ │ └── launchSettings.json │ └── packages.lock.json ├── src/ │ ├── Ocelot/ │ │ ├── Administration/ │ │ │ ├── AdministrationPath.cs │ │ │ ├── FileConfigurationController.cs │ │ │ ├── IAdministrationPath.cs │ │ │ └── OutputCacheController.cs │ │ ├── Authentication/ │ │ │ └── AuthenticationMiddleware.cs │ │ ├── Authorization/ │ │ │ ├── AuthorizationMiddleware.cs │ │ │ ├── ClaimValueNotAuthorizedError.cs │ │ │ ├── ClaimsAuthorizer.cs │ │ │ ├── IClaimsAuthorizer.cs │ │ │ ├── IScopesAuthorizer.cs │ │ │ ├── ScopeNotAuthorizedError.cs │ │ │ ├── ScopesAuthorizer.cs │ │ │ ├── UnauthorizedError.cs │ │ │ └── UserDoesNotHaveClaimError.cs │ │ ├── Cache/ │ │ │ ├── CachedResponse.cs │ │ │ ├── DefaultCacheKeyGenerator.cs │ │ │ ├── DefaultMemoryCache.cs │ │ │ ├── ICacheKeyGenerator.cs │ │ │ ├── IOcelotCache.cs │ │ │ ├── MD5Helper.cs │ │ │ └── OutputCacheMiddleware.cs │ │ ├── Claims/ │ │ │ ├── AddClaimsToRequest.cs │ │ │ ├── IAddClaimsToRequest.cs │ │ │ └── Middleware/ │ │ │ └── ClaimsToClaimsMiddleware.cs │ │ ├── Configuration/ │ │ │ ├── AuthenticationOptions.cs │ │ │ ├── Builder/ │ │ │ │ ├── DownstreamRouteBuilder.cs │ │ │ │ ├── MetadataOptionsBuilder.cs │ │ │ │ ├── ServiceProviderConfigurationBuilder.cs │ │ │ │ └── UpstreamPathTemplateBuilder.cs │ │ │ ├── CacheOptions.cs │ │ │ ├── ChangeTracking/ │ │ │ │ ├── IOcelotConfigurationChangeTokenSource.cs │ │ │ │ ├── OcelotConfigurationChangeToken.cs │ │ │ │ ├── OcelotConfigurationChangeTokenSource.cs │ │ │ │ └── OcelotConfigurationMonitor.cs │ │ │ ├── ClaimToThing.cs │ │ │ ├── Creator/ │ │ │ │ ├── AddHeader.cs │ │ │ │ ├── AggregatesCreator.cs │ │ │ │ ├── AuthenticationOptionsCreator.cs │ │ │ │ ├── CacheOptionsCreator.cs │ │ │ │ ├── ClaimsToThingCreator.cs │ │ │ │ ├── ConfigurationCreator.cs │ │ │ │ ├── DefaultMetadataCreator.cs │ │ │ │ ├── DownstreamAddressesCreator.cs │ │ │ │ ├── DynamicRoutesCreator.cs │ │ │ │ ├── FileInternalConfigurationCreator.cs │ │ │ │ ├── HeaderFindAndReplaceCreator.cs │ │ │ │ ├── HeaderTransformations.cs │ │ │ │ ├── HttpHandlerOptionsCreator.cs │ │ │ │ ├── HttpVersionCreator.cs │ │ │ │ ├── HttpVersionPolicyCreator.cs │ │ │ │ ├── IAggregatesCreator.cs │ │ │ │ ├── IAuthenticationOptionsCreator.cs │ │ │ │ ├── ICacheOptionsCreator.cs │ │ │ │ ├── IClaimsToThingCreator.cs │ │ │ │ ├── IConfigurationCreator.cs │ │ │ │ ├── IDownstreamAddressesCreator.cs │ │ │ │ ├── IDynamicsCreator.cs │ │ │ │ ├── IHeaderFindAndReplaceCreator.cs │ │ │ │ ├── IHttpHandlerOptionsCreator.cs │ │ │ │ ├── IInternalConfigurationCreator.cs │ │ │ │ ├── ILoadBalancerOptionsCreator.cs │ │ │ │ ├── IMetadataCreator.cs │ │ │ │ ├── IQoSOptionsCreator.cs │ │ │ │ ├── IRateLimitOptionsCreator.cs │ │ │ │ ├── IRequestIdKeyCreator.cs │ │ │ │ ├── IRouteKeyCreator.cs │ │ │ │ ├── IRoutesCreator.cs │ │ │ │ ├── ISecurityOptionsCreator.cs │ │ │ │ ├── IServiceProviderConfigurationCreator.cs │ │ │ │ ├── IUpstreamHeaderTemplatePatternCreator.cs │ │ │ │ ├── IUpstreamTemplatePatternCreator.cs │ │ │ │ ├── IVersionCreator.cs │ │ │ │ ├── IVersionPolicyCreator.cs │ │ │ │ ├── LoadBalancerOptionsCreator.cs │ │ │ │ ├── QoSOptionsCreator.cs │ │ │ │ ├── RateLimitOptionsCreator.cs │ │ │ │ ├── RequestIdKeyCreator.cs │ │ │ │ ├── RouteKeyCreator.cs │ │ │ │ ├── SecurityOptionsCreator.cs │ │ │ │ ├── ServiceProviderConfigurationCreator.cs │ │ │ │ ├── StaticRoutesCreator.cs │ │ │ │ ├── UpstreamHeaderTemplatePatternCreator.cs │ │ │ │ ├── UpstreamTemplatePatternCreator.cs │ │ │ │ └── VersionPolicies.cs │ │ │ ├── DownstreamHostAndPort.cs │ │ │ ├── DownstreamRoute.cs │ │ │ ├── File/ │ │ │ │ ├── AggregateRouteConfig.cs │ │ │ │ ├── FileAggregateRoute.cs │ │ │ │ ├── FileAuthenticationOptions.cs │ │ │ │ ├── FileCacheOptions.cs │ │ │ │ ├── FileConfiguration.cs │ │ │ │ ├── FileDynamicRoute.cs │ │ │ │ ├── FileGlobalAuthenticationOptions.cs │ │ │ │ ├── FileGlobalCacheOptions.cs │ │ │ │ ├── FileGlobalConfiguration.cs │ │ │ │ ├── FileGlobalHttpHandlerOptions.cs │ │ │ │ ├── FileGlobalLoadBalancerOptions.cs │ │ │ │ ├── FileGlobalQoSOptions.cs │ │ │ │ ├── FileGlobalRateLimit.cs │ │ │ │ ├── FileGlobalRateLimitByAspNetRule.cs │ │ │ │ ├── FileGlobalRateLimitByHeaderRule.cs │ │ │ │ ├── FileGlobalRateLimitByIpRule.cs │ │ │ │ ├── FileGlobalRateLimitByMethodRule.cs │ │ │ │ ├── FileGlobalRateLimiting.cs │ │ │ │ ├── FileHostAndPort.cs │ │ │ │ ├── FileHttpHandlerOptions.cs │ │ │ │ ├── FileLoadBalancerOptions.cs │ │ │ │ ├── FileMetadataOptions.cs │ │ │ │ ├── FileQoSOptions.cs │ │ │ │ ├── FileRateLimitByAspNetRule.cs │ │ │ │ ├── FileRateLimitByHeaderRule.cs │ │ │ │ ├── FileRateLimitByIpRule.cs │ │ │ │ ├── FileRateLimitByMethodRule.cs │ │ │ │ ├── FileRateLimitRule.cs │ │ │ │ ├── FileRateLimiting.cs │ │ │ │ ├── FileRoute.cs │ │ │ │ ├── FileRouteBase.cs │ │ │ │ ├── FileSecurityOptions.cs │ │ │ │ ├── FileServiceDiscoveryProvider.cs │ │ │ │ ├── IRouteGroup.cs │ │ │ │ ├── IRouteGrouping.cs │ │ │ │ ├── IRouteRateLimiting.cs │ │ │ │ └── IRouteUpstream.cs │ │ │ ├── HeaderFindAndReplace.cs │ │ │ ├── HttpHandlerOptions.cs │ │ │ ├── IInternalConfiguration.cs │ │ │ ├── InternalConfiguration.cs │ │ │ ├── LoadBalancerOptions.cs │ │ │ ├── MetadataOptions.cs │ │ │ ├── Parser/ │ │ │ │ ├── ClaimToThingConfigurationParser.cs │ │ │ │ ├── IClaimToThingConfigurationParser.cs │ │ │ │ ├── InstructionNotForClaimsError.cs │ │ │ │ ├── NoInstructionsError.cs │ │ │ │ └── ParsingConfigurationHeaderError.cs │ │ │ ├── QoSOptions.cs │ │ │ ├── RateLimitOptions.cs │ │ │ ├── RateLimitRule.cs │ │ │ ├── Repository/ │ │ │ │ ├── ConsulFileConfigurationPollerOption.cs │ │ │ │ ├── DiskFileConfigurationRepository.cs │ │ │ │ ├── FileConfigurationPoller.cs │ │ │ │ ├── IFileConfigurationPollerOptions.cs │ │ │ │ ├── IFileConfigurationRepository.cs │ │ │ │ ├── IInternalConfigurationRepository.cs │ │ │ │ ├── InMemoryFileConfigurationPollerOptions.cs │ │ │ │ └── InMemoryInternalConfigurationRepository.cs │ │ │ ├── Route.cs │ │ │ ├── SecurityOptions.cs │ │ │ ├── ServiceProviderConfiguration.cs │ │ │ ├── Setter/ │ │ │ │ ├── FileAndInternalConfigurationSetter.cs │ │ │ │ └── IFileConfigurationSetter.cs │ │ │ └── Validator/ │ │ │ ├── ConfigurationValidationResult.cs │ │ │ ├── FileAuthenticationOptionsValidator.cs │ │ │ ├── FileConfigurationFluentValidator.cs │ │ │ ├── FileGlobalConfigurationFluentValidator.cs │ │ │ ├── FileQoSOptionsFluentValidator.cs │ │ │ ├── FileValidationFailedError.cs │ │ │ ├── HostAndPortValidator.cs │ │ │ ├── IConfigurationValidator.cs │ │ │ └── RouteFluentValidator.cs │ │ ├── DependencyInjection/ │ │ │ ├── ConfigurationBuilderExtensions.cs │ │ │ ├── Features.cs │ │ │ ├── IOcelotBuilder.cs │ │ │ ├── MergeOcelotJson.cs │ │ │ ├── OcelotBuilder.cs │ │ │ └── ServiceCollectionExtensions.cs │ │ ├── DownstreamPathManipulation/ │ │ │ ├── ChangeDownstreamPathTemplate.cs │ │ │ ├── IChangeDownstreamPathTemplate.cs │ │ │ └── Middleware/ │ │ │ └── ClaimsToDownstreamPathMiddleware.cs │ │ ├── DownstreamRouteFinder/ │ │ │ ├── DownstreamRouteHolder.cs │ │ │ ├── Finder/ │ │ │ │ ├── DiscoveryDownstreamRouteFinder.cs │ │ │ │ ├── DownstreamRouteFinder.cs │ │ │ │ ├── DownstreamRouteProviderFactory.cs │ │ │ │ ├── IDownstreamRouteProvider.cs │ │ │ │ ├── IDownstreamRouteProviderFactory.cs │ │ │ │ └── UnableToFindDownstreamRouteError.cs │ │ │ ├── HeaderMatcher/ │ │ │ │ ├── HeaderPlaceholderNameAndValueFinder.cs │ │ │ │ ├── HeadersToHeaderTemplatesMatcher.cs │ │ │ │ ├── IHeaderPlaceholderNameAndValueFinder.cs │ │ │ │ └── IHeadersToHeaderTemplatesMatcher.cs │ │ │ ├── Middleware/ │ │ │ │ └── DownstreamRouteFinderMiddleware.cs │ │ │ └── UrlMatcher/ │ │ │ ├── IPlaceholderNameAndValueFinder.cs │ │ │ ├── IUrlPathToUrlTemplateMatcher.cs │ │ │ ├── PlaceholderNameAndValue.cs │ │ │ ├── RegExUrlMatcher.cs │ │ │ ├── UrlMatch.cs │ │ │ └── UrlPathPlaceholderNameAndValueFinder.cs │ │ ├── DownstreamUrlCreator/ │ │ │ ├── DownstreamPathPlaceholderReplacer.cs │ │ │ ├── DownstreamUrlCreatorMiddleware.cs │ │ │ └── IDownstreamPathPlaceholderReplacer.cs │ │ ├── Errors/ │ │ │ ├── Error.cs │ │ │ ├── Middleware/ │ │ │ │ └── ExceptionHandlerMiddleware.cs │ │ │ ├── OcelotErrorCode.cs │ │ │ └── RequestTimedOutError.cs │ │ ├── Headers/ │ │ │ ├── AddHeadersToRequest.cs │ │ │ ├── AddHeadersToResponse.cs │ │ │ ├── HttpContextRequestHeaderReplacer.cs │ │ │ ├── HttpResponseHeaderReplacer.cs │ │ │ ├── IAddHeadersToRequest.cs │ │ │ ├── IAddHeadersToResponse.cs │ │ │ ├── IHttpContextRequestHeaderReplacer.cs │ │ │ ├── IHttpResponseHeaderReplacer.cs │ │ │ ├── IRemoveOutputHeaders.cs │ │ │ ├── Middleware/ │ │ │ │ ├── ClaimsToHeadersMiddleware.cs │ │ │ │ └── HttpHeadersTransformationMiddleware.cs │ │ │ └── RemoveOutputHeaders.cs │ │ ├── Infrastructure/ │ │ │ ├── CannotAddPlaceholderError.cs │ │ │ ├── CannotRemovePlaceholderError.cs │ │ │ ├── Claims/ │ │ │ │ ├── CannotFindClaimError.cs │ │ │ │ ├── ClaimsParser.cs │ │ │ │ └── IClaimsParser.cs │ │ │ ├── ConfigAwarePlaceholders.cs │ │ │ ├── CouldNotFindPlaceholderError.cs │ │ │ ├── DelayedMessage.cs │ │ │ ├── DesignPatterns/ │ │ │ │ └── Retry.cs │ │ │ ├── Extensions/ │ │ │ │ ├── ErrorListExtensions.cs │ │ │ │ ├── HttpContextExtensions.cs │ │ │ │ ├── HttpRequestExtensions.cs │ │ │ │ ├── IEnumerableExtensions.cs │ │ │ │ ├── Int32Extensions.cs │ │ │ │ ├── StringBuilderExtensions.cs │ │ │ │ └── StringExtensions.cs │ │ │ ├── FrameworkDescription.cs │ │ │ ├── IBus.cs │ │ │ ├── IFrameworkDescription.cs │ │ │ ├── IPlaceholders.cs │ │ │ ├── InMemoryBus.cs │ │ │ ├── Placeholders.cs │ │ │ ├── RegexGlobal.cs │ │ │ └── RequestData/ │ │ │ ├── CannotAddDataError.cs │ │ │ ├── CannotFindDataError.cs │ │ │ ├── HttpDataRepository.cs │ │ │ └── IRequestScopedDataRepository.cs │ │ ├── LoadBalancer/ │ │ │ ├── Balancers/ │ │ │ │ ├── CookieStickySessions.cs │ │ │ │ ├── LeastConnection.cs │ │ │ │ ├── NoLoadBalancer.cs │ │ │ │ └── RoundRobin.cs │ │ │ ├── Creators/ │ │ │ │ ├── CookieStickySessionsCreator.cs │ │ │ │ ├── DelegateInvokingLoadBalancerCreator.cs │ │ │ │ ├── LeastConnectionCreator.cs │ │ │ │ ├── NoLoadBalancerCreator.cs │ │ │ │ └── RoundRobinCreator.cs │ │ │ ├── Errors/ │ │ │ │ ├── CouldNotFindLoadBalancerCreatorError.cs │ │ │ │ ├── InvokingLoadBalancerCreatorError.cs │ │ │ │ ├── ServicesAreEmptyError.cs │ │ │ │ ├── ServicesAreNullError.cs │ │ │ │ └── UnableToFindLoadBalancerError.cs │ │ │ ├── Interfaces/ │ │ │ │ ├── ILoadBalancer.cs │ │ │ │ ├── ILoadBalancerCreator.cs │ │ │ │ ├── ILoadBalancerFactory.cs │ │ │ │ └── ILoadBalancerHouse.cs │ │ │ ├── Lease.cs │ │ │ ├── LeaseEventArgs.cs │ │ │ ├── LoadBalancerFactory.cs │ │ │ ├── LoadBalancerHouse.cs │ │ │ ├── LoadBalancingMiddleware.cs │ │ │ └── StickySession.cs │ │ ├── Logging/ │ │ │ ├── IOcelotLogger.cs │ │ │ ├── IOcelotLoggerFactory.cs │ │ │ ├── IOcelotTracer.cs │ │ │ ├── ITracingHandler.cs │ │ │ ├── ITracingHandlerFactory.cs │ │ │ ├── OcelotDiagnosticListener.cs │ │ │ ├── OcelotHttpTracingHandler.cs │ │ │ ├── OcelotLogger.cs │ │ │ ├── OcelotLoggerFactory.cs │ │ │ └── TracingHandlerFactory.cs │ │ ├── Metadata/ │ │ │ └── DownstreamRouteExtensions.cs │ │ ├── Middleware/ │ │ │ ├── BaseUrlFinder.cs │ │ │ ├── ConfigurationMiddleware.cs │ │ │ ├── DownstreamResponse.cs │ │ │ ├── Header.cs │ │ │ ├── HttpItemsExtensions.cs │ │ │ ├── IBaseUrlFinder.cs │ │ │ ├── OcelotMiddleware.cs │ │ │ ├── OcelotMiddlewareConfigurationDelegate.cs │ │ │ ├── OcelotMiddlewareExtensions.cs │ │ │ ├── OcelotPipelineConfiguration.cs │ │ │ ├── OcelotPipelineExtensions.cs │ │ │ └── UnauthenticatedError.cs │ │ ├── Multiplexer/ │ │ │ ├── CouldNotFindAggregatorError.cs │ │ │ ├── IDefinedAggregator.cs │ │ │ ├── IDefinedAggregatorProvider.cs │ │ │ ├── IResponseAggregator.cs │ │ │ ├── IResponseAggregatorFactory.cs │ │ │ ├── InMemoryResponseAggregatorFactory.cs │ │ │ ├── MultiplexingMiddleware.cs │ │ │ ├── ServiceLocatorDefinedAggregatorProvider.cs │ │ │ ├── SimpleJsonResponseAggregator.cs │ │ │ └── UserDefinedResponseAggregator.cs │ │ ├── Ocelot.csproj │ │ ├── QualityOfService/ │ │ │ ├── IQosFactory.cs │ │ │ ├── NoQosDelegatingHandler.cs │ │ │ ├── QosDelegatingHandlerDelegate.cs │ │ │ ├── QosFactory.cs │ │ │ └── UnableToFindQoSProviderError.cs │ │ ├── QueryStrings/ │ │ │ ├── AddQueriesToRequest.cs │ │ │ ├── ClaimsToQueryStringMiddleware.cs │ │ │ └── IAddQueriesToRequest.cs │ │ ├── RateLimiting/ │ │ │ ├── ClientRequestIdentity.cs │ │ │ ├── DistributedCacheRateLimitStorage.cs │ │ │ ├── IRateLimitStorage.cs │ │ │ ├── IRateLimiting.cs │ │ │ ├── MemoryCacheRateLimitStorage.cs │ │ │ ├── QuotaExceededError.cs │ │ │ ├── RateLimitCounter.cs │ │ │ ├── RateLimitHeaders.cs │ │ │ ├── RateLimiting.cs │ │ │ ├── RateLimitingHeaders.cs │ │ │ └── RateLimitingMiddleware.cs │ │ ├── Request/ │ │ │ ├── Creator/ │ │ │ │ ├── DownstreamRequestCreator.cs │ │ │ │ └── IDownstreamRequestCreator.cs │ │ │ ├── Mapper/ │ │ │ │ ├── IRequestMapper.cs │ │ │ │ ├── PayloadTooLargeError.cs │ │ │ │ ├── RequestMapper.cs │ │ │ │ ├── StreamHttpContent.cs │ │ │ │ └── UnmappableRequestError.cs │ │ │ └── Middleware/ │ │ │ ├── DownstreamRequest.cs │ │ │ └── DownstreamRequestInitialiserMiddleware.cs │ │ ├── RequestId/ │ │ │ ├── DefaultRequestIdKey.cs │ │ │ ├── Middleware/ │ │ │ │ └── RequestIdMiddleware.cs │ │ │ └── RequestId.cs │ │ ├── Requester/ │ │ │ ├── ConnectionToDownstreamServiceError.cs │ │ │ ├── DelegatingHandlerFactory.cs │ │ │ ├── GlobalDelegatingHandler.cs │ │ │ ├── HttpExceptionToErrorMapper.cs │ │ │ ├── IDelegatingHandlerFactory.cs │ │ │ ├── IExceptionToErrorMapper.cs │ │ │ ├── IHttpRequester.cs │ │ │ ├── IMessageInvokerPool.cs │ │ │ ├── MessageInvokerHttpRequester.cs │ │ │ ├── MessageInvokerPool.cs │ │ │ ├── Middleware/ │ │ │ │ └── HttpRequesterMiddleware.cs │ │ │ ├── RequestCanceledError.cs │ │ │ ├── ServiceCollectionExtensions.cs │ │ │ ├── TimeoutDelegatingHandler.cs │ │ │ └── UnableToCompleteRequestError.cs │ │ ├── Responder/ │ │ │ ├── ErrorsToHttpStatusCodeMapper.cs │ │ │ ├── HttpContextResponder.cs │ │ │ ├── IErrorsToHttpStatusCodeMapper.cs │ │ │ ├── IHttpResponder.cs │ │ │ └── Middleware/ │ │ │ └── ResponderMiddleware.cs │ │ ├── Responses/ │ │ │ ├── ErrorResponse.cs │ │ │ ├── OkResponse.cs │ │ │ └── Response.cs │ │ ├── Security/ │ │ │ ├── IPSecurity/ │ │ │ │ └── IPSecurityPolicy.cs │ │ │ ├── ISecurityPolicy.cs │ │ │ └── Middleware/ │ │ │ └── SecurityMiddleware.cs │ │ ├── ServiceDiscovery/ │ │ │ ├── Configuration/ │ │ │ │ └── ServiceFabricConfiguration.cs │ │ │ ├── IServiceDiscoveryProviderFactory.cs │ │ │ ├── Providers/ │ │ │ │ ├── ConfigurationServiceProvider.cs │ │ │ │ ├── IServiceDiscoveryProvider.cs │ │ │ │ └── ServiceFabricServiceDiscoveryProvider.cs │ │ │ ├── ServiceDiscoveryFinderDelegate.cs │ │ │ ├── ServiceDiscoveryProviderFactory.cs │ │ │ └── UnableToFindServiceDiscoveryProviderError.cs │ │ ├── Usings.cs │ │ ├── Values/ │ │ │ ├── DownstreamPath.cs │ │ │ ├── DownstreamPathTemplate.cs │ │ │ ├── Service.cs │ │ │ ├── ServiceHostAndPort.cs │ │ │ ├── UpstreamHeaderTemplate.cs │ │ │ └── UpstreamPathTemplate.cs │ │ ├── WebSockets/ │ │ │ ├── ClientWebSocketConnector.cs │ │ │ ├── ClientWebSocketOptionsProxy.cs │ │ │ ├── ClientWebSocketProxy.cs │ │ │ ├── IClientWebSocket.cs │ │ │ ├── IClientWebSocketOptions.cs │ │ │ ├── IWebSocketsFactory.cs │ │ │ ├── WebSocketsFactory.cs │ │ │ └── WebSocketsProxyMiddleware.cs │ │ └── packages.lock.json │ ├── Ocelot.Provider.Consul/ │ │ ├── Consul.cs │ │ ├── ConsulClientFactory.cs │ │ ├── ConsulFileConfigurationRepository.cs │ │ ├── ConsulMiddlewareConfigurationProvider.cs │ │ ├── ConsulProviderFactory.cs │ │ ├── ConsulRegistryConfiguration.cs │ │ ├── DefaultConsulServiceBuilder.cs │ │ ├── Interfaces/ │ │ │ ├── IConsulClientFactory.cs │ │ │ └── IConsulServiceBuilder.cs │ │ ├── Ocelot.Provider.Consul.csproj │ │ ├── OcelotBuilderExtensions.cs │ │ ├── PollConsul.cs │ │ ├── UnableToSetConfigInConsulError.cs │ │ ├── Usings.cs │ │ └── packages.lock.json │ ├── Ocelot.Provider.Eureka/ │ │ ├── Eureka.cs │ │ ├── EurekaMiddlewareConfigurationProvider.cs │ │ ├── EurekaProviderFactory.cs │ │ ├── Ocelot.Provider.Eureka.csproj │ │ ├── OcelotBuilderExtensions.cs │ │ ├── Usings.cs │ │ └── packages.lock.json │ ├── Ocelot.Provider.Kubernetes/ │ │ ├── EndPointClientV1.cs │ │ ├── Interfaces/ │ │ │ ├── IEndPointClient.cs │ │ │ ├── IKubeApiClientFactory.cs │ │ │ ├── IKubeServiceBuilder.cs │ │ │ └── IKubeServiceCreator.cs │ │ ├── Kube.cs │ │ ├── KubeApiClientExtensions.cs │ │ ├── KubeApiClientFactory.cs │ │ ├── KubeRegistryConfiguration.cs │ │ ├── KubeServiceBuilder.cs │ │ ├── KubeServiceCreator.cs │ │ ├── KubernetesProviderFactory.cs │ │ ├── ObservableExtensions.cs │ │ ├── Ocelot.Provider.Kubernetes.csproj │ │ ├── OcelotBuilderExtensions.cs │ │ ├── PollKube.cs │ │ ├── Usings.cs │ │ ├── WatchKube.cs │ │ └── packages.lock.json │ └── Ocelot.Provider.Polly/ │ ├── CircuitBreakerStrategy.cs │ ├── Interfaces/ │ │ └── IPollyQoSResiliencePipelineProvider.cs │ ├── Ocelot.Provider.Polly.csproj │ ├── OcelotBuilderExtensions.cs │ ├── OcelotResiliencePipelineKey.cs │ ├── PollyQoSResiliencePipelineProvider.cs │ ├── PollyResiliencePipelineDelegatingHandler.cs │ ├── TimeoutStrategy.cs │ ├── Usings.cs │ └── packages.lock.json ├── test/ │ ├── Ocelot.AcceptanceTests/ │ │ ├── .gitignore │ │ ├── Administration/ │ │ │ ├── AdministrationSteps.cs │ │ │ ├── CacheManagerTests.cs │ │ │ └── OcelotBuilderExtensions.cs │ │ ├── AggregateTests.cs │ │ ├── Authentication/ │ │ │ ├── AuthenticationTests.cs │ │ │ └── MultipleAuthSchemesFeatureTests.cs │ │ ├── Authorization/ │ │ │ ├── AuthorizationSteps.cs │ │ │ └── AuthorizationTests.cs │ │ ├── Caching/ │ │ │ └── CachingTests.cs │ │ ├── CancelRequestTests.cs │ │ ├── CannotStartOcelotTests.cs │ │ ├── CaseSensitiveRoutingTests.cs │ │ ├── ConcurrentSteps.cs │ │ ├── Configuration/ │ │ │ ├── ConfigurationInConsulTests.cs │ │ │ ├── ConfigurationMergeTests.cs │ │ │ ├── ConfigurationReloadTests.cs │ │ │ ├── DownstreamHttpVersionTests.cs │ │ │ └── TimeoutTests.cs │ │ ├── ContentTests.cs │ │ ├── Core/ │ │ │ ├── LoadTests.cs │ │ │ └── ThreadSafeHeadersTests.cs │ │ ├── CustomMiddlewareTests.cs │ │ ├── DefaultVersionPolicyTests.cs │ │ ├── GzipTests.cs │ │ ├── HttpDelegatingHandlersTests.cs │ │ ├── LoadBalancer/ │ │ │ ├── CookieStickySessionsTests.cs │ │ │ ├── ILoadBalancerAnalyzer.cs │ │ │ ├── LeastConnectionAnalyzer.cs │ │ │ ├── LeastConnectionAnalyzerCreator.cs │ │ │ ├── LoadBalancerAnalyzer.cs │ │ │ ├── LoadBalancerTests.cs │ │ │ ├── RoundRobinAnalyzer.cs │ │ │ └── RoundRobinAnalyzerCreator.cs │ │ ├── LogLevelTests.cs │ │ ├── Logging/ │ │ │ ├── MemoryLogger.cs │ │ │ └── TestLoggerFactory.cs │ │ ├── Metadata/ │ │ │ └── DownstreamMetadataTests.cs │ │ ├── Ocelot.AcceptanceTests.csproj │ │ ├── Properties/ │ │ │ ├── AssemblyInfo.cs │ │ │ ├── BddfyConfig.cs │ │ │ └── GlobalSuppressions.cs │ │ ├── QualityOfService/ │ │ │ ├── PollyQoSTests.cs │ │ │ └── QosSteps.cs │ │ ├── RateLimiting/ │ │ │ ├── ClientHeaderRateLimitingTests.cs │ │ │ └── RateLimitingSteps.cs │ │ ├── ReasonPhraseTests.cs │ │ ├── Request/ │ │ │ ├── RequestMapperTests.cs │ │ │ └── StreamContentTests.cs │ │ ├── RequestIdTests.cs │ │ ├── Requester/ │ │ │ ├── MessageInvokerPoolTests.cs │ │ │ ├── PayloadTooLargeTests.cs │ │ │ ├── RequesterSteps.cs │ │ │ ├── TestMessageInvokerPool.cs │ │ │ └── TestTracer.cs │ │ ├── ResponseCodeTests.cs │ │ ├── ReturnsErrorTests.cs │ │ ├── Routing/ │ │ │ ├── RoutingBasedOnHeadersTests.cs │ │ │ ├── RoutingTests.cs │ │ │ ├── RoutingWithQueryStringTests.cs │ │ │ └── UpstreamHostTests.cs │ │ ├── Security/ │ │ │ └── SecurityOptionsTests.cs │ │ ├── SequentialTests.cs │ │ ├── ServiceDiscovery/ │ │ │ ├── ConsulAgentServiceExtensions.cs │ │ │ ├── ConsulConfigurationInConsulTests.cs │ │ │ ├── ConsulIntegrationTests.cs │ │ │ ├── ConsulServiceDiscoveryTests.cs │ │ │ ├── ConsulTwoDownstreamServicesTests.cs │ │ │ ├── ConsulWebSocketTests.cs │ │ │ ├── DynamicRoutingTests.cs │ │ │ ├── EurekaServiceDiscoveryTests.cs │ │ │ ├── KubeIntegrationTests.cs │ │ │ ├── KubernetesServiceDiscoveryTests.cs │ │ │ ├── PollKubeConcurrencyIntegrationTests.cs │ │ │ ├── PollKubeIntegrationTests.cs │ │ │ └── ServiceFabricTests.cs │ │ ├── SslTests.cs │ │ ├── StartupTests.cs │ │ ├── Steps.cs │ │ ├── Transformations/ │ │ │ ├── ClaimsToDownstreamPathTests.cs │ │ │ ├── ClaimsToHeadersForwardingTests.cs │ │ │ ├── ClaimsToQueryStringForwardingTests.cs │ │ │ ├── HeaderTests.cs │ │ │ └── MethodTests.cs │ │ ├── Usings.cs │ │ ├── WebSockets/ │ │ │ ├── ClientWebSocketTests.cs │ │ │ ├── WebSocketsFactoryTests.cs │ │ │ └── WebSocketsSteps.cs │ │ ├── appsettings.json │ │ ├── appsettings.product.json │ │ ├── mycert2.crt │ │ └── packages.lock.json │ ├── Ocelot.Benchmarks/ │ │ ├── AllTheThingsBenchmarks.cs │ │ ├── BenchmarkSteps.cs │ │ ├── DownstreamRouteFinderMiddlewareBenchmarks.cs │ │ ├── ExceptionHandlerMiddlewareBenchmarks.cs │ │ ├── MsLoggerBenchmarks.cs │ │ ├── Ocelot.Benchmarks.csproj │ │ ├── PayloadBenchmarks.cs │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── AssemblyInfo.cs │ │ ├── ResponseBenchmarks.cs │ │ ├── SerilogBenchmarks.cs │ │ ├── UrlPathToUrlPathTemplateMatcherBenchmarks.cs │ │ ├── Usings.cs │ │ └── packages.lock.json │ ├── Ocelot.ManualTest/ │ │ ├── Actions/ │ │ │ ├── Basic.cs │ │ │ └── ManualTests.cs │ │ ├── DelegatingHandlers/ │ │ │ └── FakeHandler.cs │ │ ├── Dockerfile │ │ ├── Middlewares/ │ │ │ └── MetadataMiddleware.cs │ │ ├── Ocelot.ManualTest.csproj │ │ ├── Ocelot.postman_collection.json │ │ ├── Program.cs │ │ ├── Properties/ │ │ │ └── launchSettings.json │ │ ├── Tests/ │ │ │ └── Bug0930.html │ │ ├── Usings.cs │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── docker-compose.yaml │ │ ├── ocelot.identityserver4.json │ │ ├── ocelot.json │ │ ├── packages.lock.json │ │ └── tempkey.rsa │ └── Ocelot.UnitTests/ │ ├── Authentication/ │ │ ├── AuthenticationMiddlewareTests.cs │ │ ├── AuthenticationOptionsCreatorTests.cs │ │ ├── FileAuthenticationOptionsTests.cs │ │ └── FileGlobalAuthenticationOptionsTests.cs │ ├── Authorization/ │ │ ├── AuthorizationMiddlewareTests.cs │ │ ├── ClaimsAuthorizerTests.cs │ │ ├── UnauthorizedErrorTests.cs │ │ └── UserDoesNotHaveClaimErrorTests.cs │ ├── Cache/ │ │ ├── CacheOptionsCreatorTests.cs │ │ ├── CachedResponseTests.cs │ │ ├── DefaultCacheKeyGeneratorTests.cs │ │ ├── DefaultMemoryCacheTests.cs │ │ ├── FileCacheOptionsTests.cs │ │ ├── FileGlobalCacheOptionsTests.cs │ │ └── OutputCacheMiddlewareTests.cs │ ├── Claims/ │ │ ├── AddClaimsToRequestTests.cs │ │ └── ClaimsToClaimsMiddlewareTests.cs │ ├── Configuration/ │ │ ├── AggregatesCreatorTests.cs │ │ ├── ChangeTracking/ │ │ │ ├── OcelotConfigurationChangeTokenSourceTests.cs │ │ │ ├── OcelotConfigurationChangeTokenTests.cs │ │ │ └── OcelotConfigurationMonitorTests.cs │ │ ├── ClaimToThingConfigurationParserTests.cs │ │ ├── ClaimsToThingCreatorTests.cs │ │ ├── ConfigurationCreatorTests.cs │ │ ├── DefaultMetadataCreatorTests.cs │ │ ├── DownstreamAddressesCreatorTests.cs │ │ ├── DownstreamRouteExtensionsTests.cs │ │ ├── DownstreamRouteTests.cs │ │ ├── DynamicRoutesCreatorTests.cs │ │ ├── FileConfigurationSetterTests.cs │ │ ├── FileInternalConfigurationCreatorTests.cs │ │ ├── FileModels/ │ │ │ ├── FileDynamicRouteTests.cs │ │ │ ├── FileGlobalHttpHandlerOptionsTests.cs │ │ │ ├── FileMetadataOptionsTests.cs │ │ │ └── FileRouteTests.cs │ │ ├── HashCreationTests.cs │ │ ├── HeaderFindAndReplaceCreatorTests.cs │ │ ├── HeaderFindAndReplaceTests.cs │ │ ├── HttpHandlerOptionsCreatorTests.cs │ │ ├── HttpHandlerOptionsTests.cs │ │ ├── HttpVersionPolicyCreatorTests.cs │ │ ├── MetadataOptionsTests.cs │ │ ├── Parser/ │ │ │ └── ParsingConfigurationHeaderErrorTests.cs │ │ ├── Repository/ │ │ │ ├── ConsulFileConfigurationPollerOptionTests.cs │ │ │ ├── DiskFileConfigurationRepositoryTests.cs │ │ │ ├── FileConfigurationPollerTests.cs │ │ │ ├── InMemoryConfigurationRepositoryTests.cs │ │ │ └── InMemoryFileConfigurationPollerOptionsTests.cs │ │ ├── RequestIdKeyCreatorTests.cs │ │ ├── RouteKeyCreatorTests.cs │ │ ├── RouteTests.cs │ │ ├── SecurityOptionsCreatorTests.cs │ │ ├── ServiceProviderConfigurationCreatorTests.cs │ │ ├── StaticRoutesCreatorTests.cs │ │ ├── UpstreamHeaderTemplatePatternCreatorTests.cs │ │ ├── UpstreamTemplatePatternCreatorTests.cs │ │ ├── Validation/ │ │ │ ├── FileAuthenticationOptionsValidatorTests.cs │ │ │ ├── FileConfigurationFluentValidatorTests.cs │ │ │ ├── FileQoSOptionsFluentValidatorTests.cs │ │ │ ├── HostAndPortValidatorTests.cs │ │ │ └── RouteFluentValidatorTests.cs │ │ └── VersionCreatorTests.cs │ ├── Consul/ │ │ ├── AgentServiceExtensions.cs │ │ ├── ConsulFileConfigurationRepositoryTests.cs │ │ ├── ConsulProviderFactoryTests.cs │ │ ├── DefaultConsulServiceBuilderTests.cs │ │ ├── OcelotBuilderExtensionsTests.cs │ │ └── PollingConsulServiceDiscoveryProviderTests.cs │ ├── Controllers/ │ │ ├── FileConfigurationControllerTests.cs │ │ └── OutputCacheControllerTests.cs │ ├── DependencyInjection/ │ │ ├── ConfigurationBuilderExtensionsTests.cs │ │ ├── OcelotBuilderTests.cs │ │ └── ServiceCollectionExtensionsTests.cs │ ├── DownstreamPathManipulation/ │ │ ├── ChangeDownstreamPathTemplateTests.cs │ │ └── ClaimsToDownstreamPathMiddlewareTests.cs │ ├── DownstreamRouteFinder/ │ │ ├── DiscoveryDownstreamRouteFinderTests.cs │ │ ├── DownstreamRouteFinderMiddlewareTests.cs │ │ ├── DownstreamRouteFinderTests.cs │ │ ├── DownstreamRouteHolderTests.cs │ │ ├── DownstreamRouteProviderFactoryTests.cs │ │ ├── HeaderMatcher/ │ │ │ ├── HeaderPlaceholderNameAndValueFinderTests.cs │ │ │ └── HeadersToHeaderTemplatesMatcherTests.cs │ │ └── UrlMatcher/ │ │ ├── PlaceholderNameAndValueTests.cs │ │ ├── RegExUrlMatcherTests.cs │ │ └── UrlPathPlaceholderNameAndValueFinderTests.cs │ ├── DownstreamUrlCreator/ │ │ ├── DownstreamPathPlaceholderReplacerTests.cs │ │ └── DownstreamUrlCreatorMiddlewareTests.cs │ ├── Errors/ │ │ ├── ErrorTests.cs │ │ └── ExceptionHandlerMiddlewareTests.cs │ ├── Eureka/ │ │ ├── EurekaMiddlewareConfigurationProviderTests.cs │ │ ├── EurekaProviderFactoryTests.cs │ │ ├── EurekaServiceDiscoveryProviderTests.cs │ │ └── OcelotBuilderExtensionsTests.cs │ ├── FileUnitTest.cs │ ├── Headers/ │ │ ├── AddHeadersToRequestClaimToThingTests.cs │ │ ├── AddHeadersToRequestPlainTests.cs │ │ ├── AddHeadersToResponseTests.cs │ │ ├── ClaimsToHeadersMiddlewareTests.cs │ │ ├── HttpContextRequestHeaderReplacerTests.cs │ │ ├── HttpHeadersTransformationMiddlewareTests.cs │ │ ├── HttpResponseHeaderReplacerTests.cs │ │ └── RemoveHeadersTests.cs │ ├── Infrastructure/ │ │ ├── ClaimParserTests.cs │ │ ├── ConfigAwarePlaceholdersTests.cs │ │ ├── Extensions/ │ │ │ ├── ErrorListExtensionsTests.cs │ │ │ ├── HttpContextExtensionsTests.cs │ │ │ ├── HttpRequestExtensionsTests.cs │ │ │ ├── IEnumerableExtensionsTests.cs │ │ │ ├── Int32ExtensionsTests.cs │ │ │ ├── StringBuilderExtensionsTests.cs │ │ │ └── StringExtensionsTests.cs │ │ ├── HttpDataRepositoryTests.cs │ │ ├── InMemoryBusTests.cs │ │ ├── PlaceholdersTests.cs │ │ └── ScopesAuthorizerTests.cs │ ├── Kubernetes/ │ │ ├── EndpointClientV1Tests.cs │ │ ├── FakeKubeApiClientFactory.cs │ │ ├── KubeApiClientFactoryTests.cs │ │ ├── KubeServiceBuilderTests.cs │ │ ├── KubeServiceCreatorTests.cs │ │ ├── KubernetesProviderFactoryTests.cs │ │ ├── ObservableExtensionsTests.cs │ │ ├── OcelotBuilderExtensionsTests.cs │ │ ├── PollKubeTests.cs │ │ └── WatchKubeTests.cs │ ├── LoadBalancer/ │ │ ├── CookieStickySessionsCreatorTests.cs │ │ ├── CookieStickySessionsTests.cs │ │ ├── DelegateInvokingLoadBalancerCreatorTests.cs │ │ ├── LeaseEventArgsTests.cs │ │ ├── LeaseTests.cs │ │ ├── LeastConnectionCreatorTests.cs │ │ ├── LeastConnectionTests.cs │ │ ├── LoadBalancerFactoryTests.cs │ │ ├── LoadBalancerHouseTests.cs │ │ ├── LoadBalancerMiddlewareTests.cs │ │ ├── LoadBalancerOptionsCreatorTests.cs │ │ ├── LoadBalancerOptionsTests.cs │ │ ├── NoLoadBalancerCreatorTests.cs │ │ ├── NoLoadBalancerTests.cs │ │ ├── RoundRobinCreatorTests.cs │ │ └── RoundRobinTests.cs │ ├── Logging/ │ │ ├── OcelotDiagnosticListenerTests.cs │ │ ├── OcelotHttpTracingHandlerTests.cs │ │ ├── OcelotLoggerFactoryTests.cs │ │ ├── OcelotLoggerTests.cs │ │ ├── OcelotLoggerTestsForDisposal.cs │ │ └── TracingHandlerFactoryTests.cs │ ├── Middleware/ │ │ ├── BaseUrlFinderTests.cs │ │ ├── OcelotPipelineExtensionsTests.cs │ │ └── OcelotPiplineBuilderTests.cs │ ├── Multiplexing/ │ │ ├── DefinedAggregatorProviderTests.cs │ │ ├── MultiplexingMiddlewareTests.cs │ │ ├── ResponseAggregatorFactoryTests.cs │ │ ├── SimpleJsonResponseAggregatorTests.cs │ │ └── UserDefinedResponseAggregatorTests.cs │ ├── Ocelot.UnitTests.csproj │ ├── Polly/ │ │ ├── CircuitBreakerStrategyTests.cs │ │ ├── OcelotBuilderExtensionsTests.cs │ │ ├── PollyQoSResiliencePipelineProviderTests.cs │ │ ├── PollyResiliencePipelineDelegatingHandlerTests.cs │ │ └── TimeoutStrategyTests.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── QualityOfService/ │ │ ├── FileGlobalQoSOptionsTests.cs │ │ ├── FileQoSOptionsTests.cs │ │ ├── QoSFactoryTests.cs │ │ ├── QoSOptionsCreatorTests.cs │ │ └── QoSOptionsTests.cs │ ├── QueryStrings/ │ │ ├── AddQueriesToRequestTests.cs │ │ └── ClaimsToQueryStringMiddlewareTests.cs │ ├── RateLimiting/ │ │ ├── DistributedCacheRateLimitStorageTests.cs │ │ ├── FileGlobalRateLimitingTests.cs │ │ ├── FileRateLimitByHeaderRuleTests.cs │ │ ├── FileRateLimitRuleTests.cs │ │ ├── FileRateLimitingTests.cs │ │ ├── MemoryCacheRateLimitStorageTests.cs │ │ ├── RateLimitCounterTests.cs │ │ ├── RateLimitHeadersTests.cs │ │ ├── RateLimitOptionsCreatorTests.cs │ │ ├── RateLimitOptionsTests.cs │ │ ├── RateLimitRuleTests.cs │ │ ├── RateLimitingHeadersTests.cs │ │ ├── RateLimitingMiddlewareTests.cs │ │ └── RateLimitingTests.cs │ ├── Repository/ │ │ └── HttpDataRepositoryTests.cs │ ├── Request/ │ │ ├── Creator/ │ │ │ └── DownstreamRequestCreatorTests.cs │ │ ├── DownstreamRequestInitialiserMiddlewareTests.cs │ │ ├── DownstreamRequestTests.cs │ │ └── Mapper/ │ │ ├── RequestMapperTests.cs │ │ └── StreamHttpContentTests.cs │ ├── RequestId/ │ │ └── RequestIdMiddlewareTests.cs │ ├── Requester/ │ │ ├── DelegatingHandlerFactoryTests.cs │ │ ├── FakeDelegatingHandler.cs │ │ ├── HttpExceptionToErrorMapperTests.cs │ │ ├── HttpRequesterMiddlewareTests.cs │ │ ├── MessageInvokerCacheKeyTests.cs │ │ ├── MessageInvokerHttpRequesterTests.cs │ │ ├── MessageInvokerPoolTests.cs │ │ └── TimeoutDelegatingHandlerTests.cs │ ├── Responder/ │ │ ├── AnyError.cs │ │ ├── ErrorsToHttpStatusCodeMapperTests.cs │ │ ├── HttpContextResponderTests.cs │ │ └── ResponderMiddlewareTests.cs │ ├── Security/ │ │ ├── IPSecurityPolicyTests.cs │ │ └── SecurityMiddlewareTests.cs │ ├── SequentialTests.cs │ ├── ServiceDiscovery/ │ │ ├── ConfigurationServiceProviderTests.cs │ │ ├── ServiceDiscoveryProviderFactoryTests.cs │ │ ├── ServiceFabricServiceDiscoveryProviderTests.cs │ │ └── ServiceRegistryTests.cs │ ├── TestRetry.cs │ ├── UnitTest.cs │ ├── UnitTests.runsettings │ ├── Usings.cs │ ├── WebSockets/ │ │ ├── ClientWebSocketConnectorTests.cs │ │ ├── ClientWebSocketOptionsProxyTests.cs │ │ ├── ClientWebSocketProxyTests.cs │ │ ├── MockWebSocket.cs │ │ ├── WebSocketsFactoryTests.cs │ │ └── WebSocketsProxyMiddlewareTests.cs │ ├── appsettings.json │ └── packages.lock.json └── testing/ ├── AcceptanceSteps.cs ├── Authentication/ │ ├── AuthenticationSteps.cs │ ├── AuthenticationTokenRequest.cs │ ├── AuthenticationTokenRequestEventArgs.cs │ └── OcelotScopes.cs ├── BearerToken.cs ├── Boxing/ │ ├── Box.cs │ ├── FileRouteBox.cs │ └── FileRouteExtensions.cs ├── FileUnit.cs ├── Ocelot.Testing.csproj ├── Ocelot.cs ├── PortFinder.cs ├── README.md ├── ServiceHandler.cs ├── StreamExtensions.cs ├── TestHostBuilder.cs ├── TestWebBuilder.cs ├── Unit.cs └── Wait.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .config/dotnet-tools.json ================================================ { "version": 1, "isRoot": true, "tools": { "cake.tool": { "version": "6.1.0", "commands": [ "dotnet-cake" ] }, "dotnet-reportgenerator-globaltool": { "version": "5.5.4", "commands": [ "reportgenerator" ], "rollForward": false } } } ================================================ FILE: .dockerignore ================================================ */*/bin */*/obj artifacts/ TestResults/ tools/ ================================================ FILE: .editorconfig ================================================ # Remove the line below if you want to inherit .editorconfig settings from higher directories root = true # XML files [*.xml] indent_style = space indent_size = 2 # C# files [*.cs] #### Core EditorConfig Options #### # Indentation and spacing indent_size = 4 indent_style = space tab_width = 4 # New line preferences end_of_line = crlf insert_final_newline = true #### .NET Coding Conventions #### # Organize usings dotnet_separate_import_directive_groups = false dotnet_sort_system_directives_first = false file_header_template = unset # this. and Me. preferences dotnet_style_qualification_for_event = false dotnet_style_qualification_for_field = false dotnet_style_qualification_for_method = false dotnet_style_qualification_for_property = false # Language keywords vs BCL types preferences dotnet_style_predefined_type_for_locals_parameters_members = true dotnet_style_predefined_type_for_member_access = true # Parentheses preferences dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity dotnet_style_parentheses_in_other_binary_operators = always_for_clarity dotnet_style_parentheses_in_other_operators = never_if_unnecessary dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity # Modifier preferences dotnet_style_require_accessibility_modifiers = for_non_interface_members # Expression-level preferences dotnet_style_coalesce_expression = true dotnet_style_collection_initializer = true dotnet_style_explicit_tuple_names = true dotnet_style_namespace_match_folder = true dotnet_style_null_propagation = true dotnet_style_object_initializer = true dotnet_style_operator_placement_when_wrapping = beginning_of_line dotnet_style_prefer_auto_properties = true dotnet_style_prefer_collection_expression = false:suggestion dotnet_style_prefer_compound_assignment = true dotnet_style_prefer_conditional_expression_over_assignment = true dotnet_style_prefer_conditional_expression_over_return = true dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed dotnet_style_prefer_inferred_anonymous_type_member_names = true dotnet_style_prefer_inferred_tuple_names = true dotnet_style_prefer_is_null_check_over_reference_equality_method = true dotnet_style_prefer_simplified_boolean_expressions = true dotnet_style_prefer_simplified_interpolation = true # Field preferences dotnet_style_readonly_field = true # Parameter preferences dotnet_code_quality_unused_parameters = all # Suppression preferences dotnet_remove_unnecessary_suppression_exclusions = none # New line preferences dotnet_style_allow_multiple_blank_lines_experimental = true dotnet_style_allow_statement_immediately_after_block_experimental = true #### C# Coding Conventions #### # var preferences csharp_style_var_elsewhere = false csharp_style_var_for_built_in_types = false csharp_style_var_when_type_is_apparent = false # Expression-bodied members csharp_style_expression_bodied_accessors = true:silent csharp_style_expression_bodied_constructors = false:silent csharp_style_expression_bodied_indexers = true:silent csharp_style_expression_bodied_lambdas = true:silent csharp_style_expression_bodied_local_functions = false:silent csharp_style_expression_bodied_methods = false:silent csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:silent # Pattern matching preferences csharp_style_pattern_matching_over_as_with_null_check = true csharp_style_pattern_matching_over_is_with_cast_check = true csharp_style_prefer_extended_property_pattern = true csharp_style_prefer_not_pattern = true csharp_style_prefer_pattern_matching = true csharp_style_prefer_switch_expression = true # Null-checking preferences csharp_style_conditional_delegate_call = true # Modifier preferences csharp_prefer_static_local_function = true csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async csharp_style_prefer_readonly_struct = true csharp_style_prefer_readonly_struct_member = true # Code-block preferences csharp_prefer_braces = true:silent csharp_prefer_simple_using_statement = true:suggestion csharp_style_namespace_declarations = block_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_primary_constructors = false:suggestion csharp_style_prefer_top_level_statements = true:silent # Expression-level preferences csharp_prefer_simple_default_expression = true csharp_style_deconstructed_variable_declaration = true csharp_style_implicit_object_creation_when_type_is_apparent = true csharp_style_inlined_variable_declaration = true csharp_style_prefer_index_operator = true csharp_style_prefer_local_over_anonymous_function = true csharp_style_prefer_null_check_over_type_check = true csharp_style_prefer_range_operator = true csharp_style_prefer_tuple_swap = true csharp_style_prefer_utf8_string_literals = true csharp_style_throw_expression = true csharp_style_unused_value_assignment_preference = discard_variable csharp_style_unused_value_expression_statement_preference = discard_variable # 'using' directive preferences csharp_using_directive_placement = outside_namespace:silent # New line preferences csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true csharp_style_allow_embedded_statements_on_same_line_experimental = true #### C# Formatting Rules #### # New line preferences csharp_new_line_before_catch = true csharp_new_line_before_else = true csharp_new_line_before_finally = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_open_brace = all csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true csharp_indent_case_contents_when_block = true csharp_indent_labels = one_less_than_current csharp_indent_switch_labels = true # Space preferences csharp_space_after_cast = false csharp_space_after_colon_in_inheritance_clause = true csharp_space_after_comma = true csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false csharp_space_before_open_square_brackets = false csharp_space_before_semicolon_in_for_statement = false csharp_space_between_empty_square_brackets = false csharp_space_between_method_call_empty_parameter_list_parentheses = false csharp_space_between_method_call_name_and_opening_parenthesis = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_space_between_method_declaration_empty_parameter_list_parentheses = false csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false # Wrapping preferences csharp_preserve_single_line_blocks = true csharp_preserve_single_line_statements = true #### Naming styles #### # Naming rules dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion dotnet_naming_rule.types_should_be_pascal_case.symbols = types dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case # Symbol specifications dotnet_naming_symbols.interface.applicable_kinds = interface dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.interface.required_modifiers = dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.types.required_modifiers = dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected dotnet_naming_symbols.non_field_members.required_modifiers = # Naming styles dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case dotnet_naming_style.begins_with_i.required_prefix = I dotnet_naming_style.begins_with_i.required_suffix = dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case [*.{cs,vb}] dotnet_style_operator_placement_when_wrapping = beginning_of_line tab_width = 4 indent_size = 4 end_of_line = crlf dotnet_style_coalesce_expression = true:suggestion insert_final_newline = true dotnet_style_null_propagation = true:suggestion dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion ================================================ FILE: .gitattributes ================================================ # 2010 *.txt -crlf # 2020 *.txt text eol=lf ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tom@threemammals.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: .github/CONTRIBUTING.md ================================================ We love to receive contributions from the community so please keep them coming :) Pull requests, issues and commentary welcome! Please complete the relevant template for issues and PRs. Sometimes it's worth getting in touch with us to discuss changes before doing any work incase this is something we are already doing or it might not make sense. We can also give advice on the easiest way to do things :) Finally we mark all existing issues as help wanted, small, medium and large effort. If you want to contribute for the first time I suggest looking at a help wanted & small effort issue :) ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ## Expected Behavior / New Feature ## Actual Behavior / Motivation for New Feature ## Steps to Reproduce the Problem 1. 1. 1. ## Specifications - Version: - Platform: - Subsystem: ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ Fixes / New Feature # ## Proposed Changes - - - ================================================ FILE: .github/steps/check-dotnet.sh ================================================ #!/bin/bash # First argument: target .NET major version (digit) # Default to 8 if no argument is provided DOTNET_VERSION="${1:-8}" # Check .NET $DOTNET_VERSION DOTNET_INFO=$(dotnet --info) echo Checking for .NET $DOTNET_VERSION SDK in dotnet info output... echo ------------------------------------------------------------- # Print matching lines echo "$DOTNET_INFO" | grep -E "^\s*${DOTNET_VERSION}\.0\.[0-9]+\s+\[/usr/share/dotnet/sdk\]" # Set environment variable based on match if echo "$DOTNET_INFO" | grep -qE "^\s*${DOTNET_VERSION}\.0\.[0-9]+\s+\[/usr/share/dotnet/sdk\]"; then echo "DOTNET${DOTNET_VERSION}_installed=true" >> "$GITHUB_ENV" else echo "DOTNET${DOTNET_VERSION}_installed=false" >> "$GITHUB_ENV" fi ================================================ FILE: .github/steps/macos.add-dns-records.sh ================================================ #!/bin/bash hosts="/etc/hosts" if [ ! -f "$hosts" ]; then echo "$hosts not found." exit 1 fi # Find the line number of the last line that starts with "##" last_index=$(grep -n '^##' "$hosts" | tail -n 1 | cut -d: -f1) # Check if the line exists if [ -z "$last_index" ]; then echo No lines start with '##' in $hosts exit 1 fi # Insert DNS-record after the last "##" line record="127.0.0.1 threemammals.com" # This 3-line sed script fixes the issue when embedded as a run-action script in GitHub Actions. # The problem prevents the workflow file from being parsed correctly, which stops the workflow from starting. sudo sed -i '' "${last_index}a\\ $record " $hosts echo "Inserted '$record' after line $last_index." echo DNS-record added to $hosts echo ------------------------ cat $hosts echo ------------------------ # The threemammals.com domain is registered for email services # https://registrar.ionos.com/domains_raa/whois # So, go ahead and clear the DNS cache sudo killall -HUP mDNSResponder ping -c 3 threemammals.com # Additional Loopback IPs echo -n "Adding multiple aliases in a loop..." for i in {2..255}; do sudo ifconfig lo0 alias 127.0.0.$i up done echo DONE echo ------------------------ echo Test Loopback IPs for i in {2..255}; do echo ping 127.0.0.$i ... ping -c 1 127.0.0.$i done ================================================ FILE: .github/steps/macos.install-certificate.sh ================================================ #!/bin/bash # Install mycert2.crt certificate crt='./test/Ocelot.AcceptanceTests/mycert2.crt' openssl version echo Moving the certificate to the trusted CA store... if [ ! -f "$crt" ]; then echo "Certificate file not found: $crt" exit 1 fi cert_root="/Library/Keychains/System.keychain" sudo security add-trusted-cert -d -r trustRoot -k "$cert_root" "$crt" echo Certificate added to trusted keychain. echo Verifying installation by listing certificates in $cert_root ... sudo security find-certificate -a -c "threemammals" -p "$cert_root" echo Verifying installation by openssl for $crt in $cert_root ... # Export the matching certificate(s) in PEM directly (security find-certificate -p) tmpcert=$(mktemp /tmp/mycert.XXXXXX.pem) # Use sudo + tee so the redirected file is written with appropriate permissions sudo security find-certificate -a -c "threemammals" -p "$cert_root" | sudo tee "$tmpcert" >/dev/null if [ ! -s "$tmpcert" ]; then echo "Failed to export certificate to $tmpcert" rm -f "$tmpcert" exit 1 fi chmod 644 "$tmpcert" openssl x509 -in "$tmpcert" -text -noout rm -f "$tmpcert" echo Installation is DONE ================================================ FILE: .github/steps/prepare-coveralls.sh ================================================ #!/bin/bash # Prepare Coveralls echo "::group::Listing environment variables" env | sort echo "::endgroup::" echo ------------ Detect coverage file ------------ coverage_1st_folder=$(ls -d /home/runner/work/Ocelot/Ocelot/artifacts/UnitTests/*/ | head -1) echo "Detected first folder : $coverage_1st_folder" coverage_file="${coverage_1st_folder%/}/coverage.cobertura.xml" echo "Detecting file $coverage_file ..." if [ -f "$coverage_file" ]; then echo "Coverage file exists." echo "COVERALLS_coverage_file_exists=true" >> $GITHUB_ENV echo "COVERALLS_coverage_file=$coverage_file" >> $GITHUB_ENV else echo "Coverage file DOES NOT exist!" echo "COVERALLS_coverage_file_exists=false" >> $GITHUB_ENV fi ================================================ FILE: .github/steps/ubuntu.add-dns-records.sh ================================================ #!/bin/bash hosts="/etc/hosts" if [ ! -f "$hosts" ]; then echo "$hosts not found." exit 1 fi sudo sed -i '$a 127.0.0.1 threemammals.com' $hosts echo DNS-record added to $hosts echo ------------------------ cat $hosts echo ------------------------ ping -c 3 threemammals.com ================================================ FILE: .github/steps/ubuntu.install-certificate.sh ================================================ #!/bin/bash # Install mycert2.crt certificate crt='./test/Ocelot.AcceptanceTests/mycert2.crt' if [ -f "$crt" ]; then echo mycert2.crt file found fi openssl version # Copy the certificate to the system's trusted CA directory echo Moving the certificate to the trusted CA store... cert='/usr/local/share/ca-certificates/mycert2.crt' sudo cp $crt $cert echo Updating the trusted certificates... sudo update-ca-certificates # This will add mycert.crt to the trusted root storage echo Verifying installation by listing in /etc/ssl/certs/ folder... sudo ls /etc/ssl/certs/ | grep mycert echo Verifying installation by openssl for $cert file... sudo chmod 644 $cert # adjusting the permissions ls -l $cert # verify ownership openssl x509 -in $cert -text -noout echo Installation is DONE ================================================ FILE: .github/steps/windows.add-dns-records.ps1 ================================================ # Add DNS-records Write-Host "Hello from PowerShell" Get-Date # Append entry to hosts file Add-Content -Path "$env:SystemRoot\System32\drivers\etc\hosts" -Value "127.0.0.1 threemammals.com" Write-Output "------------------------" Get-Content "$env:SystemRoot\System32\drivers\etc\hosts" Write-Output "------------------------" # Ping 3 times Test-Connection -ComputerName "threemammals.com" -Count 3 ================================================ FILE: .github/steps/windows.install-certificate.ps1 ================================================ Write-Host "Hello from PowerShell" Get-Date # Install mycert2.crt certificate $crt = ".\test\Ocelot.AcceptanceTests\mycert2.crt" if (Test-Path $crt) { Write-Output "mycert2.crt file found" } openssl version Write-Output "Moving the certificate to the trusted CA store..." # Import into the Local Machine Trusted Root Certification Authorities store $crt = ".\test\Ocelot.AcceptanceTests\mycert2.crt" Import-Certificate -FilePath $crt -CertStoreLocation Cert:\LocalMachine\Root Write-Output "Verifying installation by listing trusted root certificates..." # List certificates in the Trusted Root store and filter for 'mycert' Get-ChildItem -Path Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*threemammals*" } Write-Output "Verifying installation by openssl for $crt file..." $cert = Get-ChildItem Cert:\LocalMachine\Root | Where-Object { $_.Subject -like "*threemammals*" } $cert_file = "C:\temp\mycert2_installed.cer" Export-Certificate -Cert $cert -FilePath $cert_file if (Test-Path $cert_file) { Write-Output "$cert_file file found" } # Display certificate details using OpenSSL (if installed) openssl x509 -in $cert_file -text -noout Write-Output "Installation is DONE" ================================================ FILE: .github/workflows/develop.yml ================================================ name: Develop on: push: branches: - develop # - 'release/**' # for testing purposes jobs: build-windows: runs-on: windows-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: Setup .NET 10 uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/windows.add-dns-records.ps1 - name: Install certificate run: ./.github/steps/windows.install-certificate.ps1 - name: Restore run: dotnet restore --locked-mode ./Ocelot.slnx - name: Build run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj build-macos: needs: build-windows runs-on: macos-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Setup .NET 10 uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/macos.add-dns-records.sh - name: Install certificate run: ./.github/steps/macos.install-certificate.sh - name: Restore run: dotnet restore --locked-mode ./Ocelot.slnx - name: Build run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj build-linux: needs: build-macos strategy: matrix: dotnet-version: [ '8', '9', '10' ] runs-on: ubuntu-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Check .NET ${{ matrix.dotnet-version }} id: check-dotnet run: ./.github/steps/check-dotnet.sh ${{ matrix.dotnet-version }} - name: Setup .NET ${{ matrix.dotnet-version }} # if: env.DOTNET${{ matrix.dotnet-version }}_installed == 'false' uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }}.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/ubuntu.add-dns-records.sh - name: Install certificate run: ./.github/steps/ubuntu.install-certificate.sh - name: Restore run: dotnet restore --locked-mode ./Ocelot.slnx - name: Build run: dotnet build --no-restore ./Ocelot.slnx --framework net${{ matrix.dotnet-version }}.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj build-cake: needs: build-linux runs-on: ubuntu-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: SH-scripts executable run: chmod +x .github/steps/*.sh - name: Setup .NET 10 uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: Cake Build uses: cake-build/cake-action@v3 with: target: UnitTests # LatestFramework - name: Coverage files run: ./.github/steps/prepare-coveralls.sh - name: Coveralls if: env.COVERALLS_coverage_file_exists == 'true' uses: coverallsapp/github-action@v2 with: # fail-on-error: false file: ${{ env.COVERALLS_coverage_file }} continue-on-error: true - name: Codecov if: env.COVERALLS_coverage_file_exists == 'true' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: ThreeMammals/Ocelot fail_ci_if_error: true files: ${{ env.COVERALLS_coverage_file }} flags: unittests continue-on-error: true ================================================ FILE: .github/workflows/pr-closed.yml ================================================ name: PR Closed on: pull_request: types: [closed] jobs: cleanup: runs-on: ubuntu-latest env: GH_TOKEN: ${{ github.token }} steps: - name: Checkout uses: actions/checkout@v5 - name: Delete caches via gh CLI run: | pr_no="${{ github.event.pull_request.number }}" echo Pull request $pr_no cache items to be deleted... gh cache list --ref refs/pull/$pr_no/merge echo -------------------------------------- echo All cache items... gh cache list echo -------------------------------------- # gh cache list --ref refs/pull/$pr_no/merge --json id --jq '.[].id' | xargs -n1 gh cache delete # Sometimes, items produced by other workflows get reused in the PR workflow # gh cache delete --all --succeed-on-no-caches echo "PR $pr_no: deleting caches for refs/pull/$pr_no/merge..." ids=$(gh cache list --ref refs/pull/$pr_no/merge --json id --jq '.[].id') if [ -z "$ids" ]; then echo "No PR caches found." else echo "$ids" | xargs -r -I{} sh -c 'gh cache delete {} || echo "delete {} failed (ignored)"' fi echo DONE continue-on-error: true ================================================ FILE: .github/workflows/pr.yml ================================================ # This workflow will build a .NET project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net name: PR on: pull_request jobs: # build-linux: # runs-on: ubuntu-latest # continue-on-error: true # env: # # https://github.com/actions/setup-dotnet/blob/main/README.md#environment-variables # DOTNET_INSTALL_DIR: "/usr/share/dotnet" # don't override by /usr/lib/dotnet # NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages # steps: # - name: Checkout # uses: actions/checkout@v5 # - name: SH-scripts executable # run: | # chmod +x .github/steps/*.sh # ls -l .github/steps/*.sh # - name: Check .NET 10 # id: check-dotnet10 # run: ./.github/steps/check-dotnet.sh 10 # - name: Setup .NET 10 # # if: env.DOTNET10_installed == 'false' # uses: actions/setup-dotnet@v5 # with: # dotnet-version: 10.x # cache: true # cache-dependency-path: | # samples/**/packages.lock.json # src/**/packages.lock.json # test/**/packages.lock.json # - name: .NET Info # run: dotnet --info # - name: Add DNS-records # run: ./.github/steps/ubuntu.add-dns-records.sh # - name: Install certificate # run: ./.github/steps/ubuntu.install-certificate.sh # - name: Restore # run: dotnet restore --locked-mode ./Ocelot.slnx # - name: Build # run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0 # - name: Unit Tests # run: dotnet test --no-restore --no-build --verbosity normal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj # - name: Acceptance Tests # run: dotnet test --no-restore --no-build --verbosity normal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj # build-macos: # runs-on: macos-latest # continue-on-error: true # env: # NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages # steps: # - name: Checkout # uses: actions/checkout@v5 # - name: SH-scripts executable # run: | # chmod +x .github/steps/*.sh # ls -l .github/steps/*.sh # - name: Setup .NET 10 # uses: actions/setup-dotnet@v5 # with: # dotnet-version: 10.x # cache: true # cache-dependency-path: | # samples/**/packages.lock.json # src/**/packages.lock.json # test/**/packages.lock.json # - name: .NET Info # run: dotnet --info # - name: Add DNS-records # run: ./.github/steps/macos.add-dns-records.sh # - name: Install certificate # run: ./.github/steps/macos.install-certificate.sh # - name: Restore # run: dotnet restore --locked-mode ./Ocelot.slnx # - name: Build # run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0 # - name: Unit Tests # run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj # - name: Acceptance Tests # run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj # build-windows: # runs-on: windows-latest # continue-on-error: true # env: # NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages # steps: # - name: Checkout # uses: actions/checkout@v5 # - name: Setup .NET 10 # uses: actions/setup-dotnet@v5 # with: # dotnet-version: 10.x # cache: true # cache-dependency-path: | # samples/**/packages.lock.json # src/**/packages.lock.json # test/**/packages.lock.json # - name: .NET Info # run: dotnet --info # - name: Add DNS-records # run: ./.github/steps/windows.add-dns-records.ps1 # - name: Install certificate # run: ./.github/steps/windows.install-certificate.ps1 # - name: Restore # run: dotnet restore --locked-mode ./Ocelot.slnx # - name: Build # run: dotnet build --no-restore ./Ocelot.slnx --framework net10.0 # - name: Unit Tests # run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj # - name: Acceptance Tests # run: dotnet test --no-restore --no-build --verbosity minimal --framework net10.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj # build-success: # needs: [build-linux, build-macos, build-windows] # runs-on: ubuntu-latest # if: ${{ always() }} # run even Linux / MacOS / Windows build fails # steps: # - name: Decide success # run: | # linux_result="${{ needs.build-linux.result }}" # macos_result="${{ needs.build-macos.result }}" # windows_result="${{ needs.build-windows.result }}" # if [[ $linux_result == "success" || $macos_result == "success" || $windows_result == "success" ]]; then # echo "At least one succeeded" # else # echo "All failed" # exit 1 # fi build-cake: runs-on: ubuntu-latest # environment: Pull-Request env: # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Check .NET 10 id: check-dotnet10 run: ./.github/steps/check-dotnet.sh 10 - name: Setup .NET 10 # if: env.DOTNET10_installed == 'false' uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/ubuntu.add-dns-records.sh - name: Install certificate run: ./.github/steps/ubuntu.install-certificate.sh - name: Cake Build uses: cake-build/cake-action@v3 with: target: PullRequest verbosity: Verbose - name: Coverage files run: ./.github/steps/prepare-coveralls.sh - name: Coveralls if: env.COVERALLS_coverage_file_exists == 'true' uses: coverallsapp/github-action@v2 with: # fail-on-error: false file: ${{ env.COVERALLS_coverage_file }} continue-on-error: true - name: Codecov if: env.COVERALLS_coverage_file_exists == 'true' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: ThreeMammals/Ocelot fail_ci_if_error: true files: ${{ env.COVERALLS_coverage_file }} flags: unittests continue-on-error: true ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: branches: - main - 'release/24.**' jobs: build-windows: strategy: matrix: dotnet-version: [ '8', '9', '10' ] runs-on: windows-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: Setup .NET ${{ matrix.dotnet-version }} uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }}.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/windows.add-dns-records.ps1 - name: Install certificate run: ./.github/steps/windows.install-certificate.ps1 - name: Restore run: dotnet restore --locked-mode ./Ocelot.Release.sln - name: Build run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj build-macos: needs: build-windows strategy: matrix: dotnet-version: [ '8', '9', '10' ] runs-on: macos-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Setup .NET ${{ matrix.dotnet-version }} uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }}.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/macos.add-dns-records.sh - name: Install certificate run: ./.github/steps/macos.install-certificate.sh - name: Restore run: dotnet restore --locked-mode ./Ocelot.Release.sln - name: Build run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj build-linux: needs: build-macos strategy: matrix: dotnet-version: [ '8', '9', '10' ] runs-on: ubuntu-latest env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Checkout uses: actions/checkout@v5 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Check .NET ${{ matrix.dotnet-version }} id: check-dotnet run: ./.github/steps/check-dotnet.sh ${{ matrix.dotnet-version }} - name: Setup .NET ${{ matrix.dotnet-version }} # if: env.DOTNET${{ matrix.dotnet-version }}_installed == 'false' uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ matrix.dotnet-version }}.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: .NET Info run: dotnet --info - name: Add DNS-records run: ./.github/steps/ubuntu.add-dns-records.sh - name: Install certificate run: ./.github/steps/ubuntu.install-certificate.sh - name: Restore run: dotnet restore --locked-mode ./Ocelot.Release.sln - name: Build run: dotnet build --no-restore ./Ocelot.Release.sln --framework net${{ matrix.dotnet-version }}.0 - name: Unit Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj - name: Acceptance Tests run: dotnet test --no-restore --no-build --verbosity minimal --framework net${{ matrix.dotnet-version }}.0 --settings coverlet.runsettings ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj release-cake: needs: build-linux runs-on: ubuntu-latest environment: build.cake # TODO Rename to Release env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - name: Env Variables env: # CAKE_RELEASE_MYVAR: ${{ vars.CAKE_RELEASE_MYVAR }} # TEMP_KEY: ${{ secrets.TEMP_KEY }} # leaked secret LoL GITHUB_CONTEXT: ${{ toJson(github) }} run: | echo "github context >>>" # https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#github-context echo "$GITHUB_CONTEXT" echo "<<<" - name: Checkout uses: actions/checkout@v5 with: fetch-depth: 0 - name: SH-scripts executable run: | chmod +x .github/steps/*.sh ls -l .github/steps/*.sh - name: Check .NET 10 id: check-dotnet run: ./.github/steps/check-dotnet.sh 10 - name: Setup .NET 10 uses: actions/setup-dotnet@v5 with: dotnet-version: 10.x cache: true cache-dependency-path: | samples/**/packages.lock.json src/**/packages.lock.json test/**/packages.lock.json - name: Cake Build uses: cake-build/cake-action@v3 with: target: Release env: OCELOT_GITHUB_API_KEY: ${{ secrets.OCELOT_GITHUB_API_KEY }} OCELOT_NUGET_API_KEY_2025: ${{ secrets.OCELOT_NUGET_API_KEY_2025 }} # COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - name: Coverage files run: ./.github/steps/prepare-coveralls.sh - name: Coveralls if: env.COVERALLS_coverage_file_exists == 'true' uses: coverallsapp/github-action@v2 with: file: ${{ env.COVERALLS_coverage_file }} compare-ref: main continue-on-error: true - name: Codecov if: env.COVERALLS_coverage_file_exists == 'true' uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} slug: ThreeMammals/Ocelot fail_ci_if_error: true files: ${{ env.COVERALLS_coverage_file }} flags: unittests continue-on-error: true ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # ASP.NET Scaffolding ScaffoldingReadMe.txt # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.tlog *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Coverlet is a free, cross platform Code Coverage Tool coverage*.json coverage*.xml coverage*.info # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio 6 auto-generated project file (contains which files were open etc.) *.vbp # Visual Studio 6 workspace and project file (working project files containing files to include in project) *.dsw *.dsp # Visual Studio 6 technical files *.ncb *.aps # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # Visual Studio History (VSHistory) files .vshistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ # Fody - auto-generated XML schema FodyWeavers.xsd # VS Code files for those working on multiple tools .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json *.code-workspace # Local History for Visual Studio Code .history/ # Windows Installer files from build outputs *.cab *.msi *.msix *.msm *.msp # JetBrains Rider *.sln.iml .idea/ # Visual Studio Test Results # Quick Facts: https://file.org/extension/trx#visualstudiotestresults # Running automated tests from the command line: https://learn.microsoft.com/en-us/previous-versions/ms182486(v=vs.140) # Run unit tests with Test Explorer: https://learn.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer *.trx # OCELOT # FAKE - F# Make .fake/ !tools/packages.config tools/ # ReportGenerator dotnet tool -> https://reportgenerator.io/ **/coveragereport/ # Ocelot acceptance test config test/Ocelot.AcceptanceTests/ocelot.json # Read the Docs -> https://ocelot.readthedocs.io docs/_build/ docs/_templates/ ================================================ FILE: .readthedocs.yaml ================================================ # .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: os: ubuntu-24.04 tools: python: "latest" # You can also specify other tool versions: # nodejs: "19" # rust: "1.64" # golang: "1.19" # Build documentation in the "docs/" directory with Sphinx sphinx: configuration: docs/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: - pdf - epub # Optional but recommended, declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html python: install: - requirements: docs/requirements.txt ================================================ FILE: GitVersion.yml ================================================ mode: ContinuousDelivery branches: {} ignore: sha: [] ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT), Open Source Software, Copyright © 2016-2026 Tom Gardham-Pallister, Raman Maksimchuk and contributors. Permission is hereby granted, free of charge, to any person obtaining a copy of this open-source software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Ocelot.Samples.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.3.11520.95 d18.3 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{B37314F1-C1B5-4D38-8000-E6E96C0CBD30}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.ApiGateway", "samples\Eureka\ApiGateway\Ocelot.Samples.Eureka.ApiGateway.csproj", "{EA0E146F-2C2B-4176-B6EC-F62A587F5077}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Eureka.DownstreamService", "samples\Eureka\DownstreamService\Ocelot.Samples.Eureka.DownstreamService.csproj", "{B7317B64-2208-472D-90AC-F42B61956B79}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.GraphQL", "samples\GraphQL\Ocelot.Samples.GraphQL.csproj", "{6CCA3677-420A-4294-8D41-67CF3D818575}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.ApiGateway", "samples\Kubernetes\ApiGateway\Ocelot.Samples.Kubernetes.ApiGateway.csproj", "{721C1737-70CB-4B11-A19B-C7AAC6856CC7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Kubernetes.DownstreamService", "samples\Kubernetes\DownstreamService\Ocelot.Samples.Kubernetes.DownstreamService.csproj", "{CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\ServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{96B9F16E-C95D-425A-A419-40CB3C90CB77}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\ServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{60E14B1A-C295-453B-910E-58E09F5A28AA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "samples\ServiceFabric\ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{115F7934-3326-492A-B131-64F0EAEBAD71}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "samples\ServiceFabric\DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{6C777A20-F557-45CF-B87B-11E3C6B29A36}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.Web", "samples\Web\Ocelot.Samples.Web.csproj", "{EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Basic", "samples\Basic\Ocelot.Samples.Basic.csproj", "{3225BD01-42ED-4362-9757-28CBFF6B2D70}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Configuration", "samples\Configuration\Ocelot.Samples.Configuration.csproj", "{FE091795-7FBF-4D82-ABD6-51405F210142}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Metadata", "samples\Metadata\Ocelot.Samples.Metadata.csproj", "{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Debug|Any CPU.Build.0 = Debug|Any CPU {B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Release|Any CPU.ActiveCfg = Release|Any CPU {B37314F1-C1B5-4D38-8000-E6E96C0CBD30}.Release|Any CPU.Build.0 = Release|Any CPU {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA0E146F-2C2B-4176-B6EC-F62A587F5077}.Release|Any CPU.Build.0 = Release|Any CPU {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B7317B64-2208-472D-90AC-F42B61956B79}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7317B64-2208-472D-90AC-F42B61956B79}.Release|Any CPU.Build.0 = Release|Any CPU {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6CCA3677-420A-4294-8D41-67CF3D818575}.Debug|Any CPU.Build.0 = Debug|Any CPU {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.ActiveCfg = Release|Any CPU {6CCA3677-420A-4294-8D41-67CF3D818575}.Release|Any CPU.Build.0 = Release|Any CPU {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {721C1737-70CB-4B11-A19B-C7AAC6856CC7}.Release|Any CPU.Build.0 = Release|Any CPU {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Debug|Any CPU.Build.0 = Debug|Any CPU {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE949A5D-9D25-46E3-B59A-DA63F7ED9A59}.Release|Any CPU.Build.0 = Release|Any CPU {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Debug|Any CPU.Build.0 = Debug|Any CPU {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.ActiveCfg = Release|Any CPU {96B9F16E-C95D-425A-A419-40CB3C90CB77}.Release|Any CPU.Build.0 = Release|Any CPU {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {60E14B1A-C295-453B-910E-58E09F5A28AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {60E14B1A-C295-453B-910E-58E09F5A28AA}.Release|Any CPU.Build.0 = Release|Any CPU {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.ActiveCfg = Debug|x64 {115F7934-3326-492A-B131-64F0EAEBAD71}.Debug|Any CPU.Build.0 = Debug|x64 {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.ActiveCfg = Release|Any CPU {115F7934-3326-492A-B131-64F0EAEBAD71}.Release|Any CPU.Build.0 = Release|Any CPU {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Debug|Any CPU.Build.0 = Debug|Any CPU {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.ActiveCfg = Release|Any CPU {6C777A20-F557-45CF-B87B-11E3C6B29A36}.Release|Any CPU.Build.0 = Release|Any CPU {EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {EA553F5C-4B94-4E4A-8C3E-0124C5EA5F6E}.Release|Any CPU.Build.0 = Release|Any CPU {3225BD01-42ED-4362-9757-28CBFF6B2D70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3225BD01-42ED-4362-9757-28CBFF6B2D70}.Debug|Any CPU.Build.0 = Debug|Any CPU {3225BD01-42ED-4362-9757-28CBFF6B2D70}.Release|Any CPU.ActiveCfg = Release|Any CPU {3225BD01-42ED-4362-9757-28CBFF6B2D70}.Release|Any CPU.Build.0 = Release|Any CPU {FE091795-7FBF-4D82-ABD6-51405F210142}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FE091795-7FBF-4D82-ABD6-51405F210142}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE091795-7FBF-4D82-ABD6-51405F210142}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE091795-7FBF-4D82-ABD6-51405F210142}.Release|Any CPU.Build.0 = Release|Any CPU {80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.Build.0 = Debug|Any CPU {80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.ActiveCfg = Release|Any CPU {80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2C1620D4-EB38-4C3E-9FC5-029FB6B2F426} EndGlobalSection EndGlobal ================================================ FILE: Ocelot.Samples.slnx ================================================ ================================================ FILE: Ocelot.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.3.11520.95 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3FA7C349-DBE8-4904-A2CE-015B8869CE6C}" ProjectSection(SolutionItems) = preProject .dockerignore = .dockerignore .editorconfig = .editorconfig .gitignore = .gitignore .readthedocs.yaml = .readthedocs.yaml build.cake = build.cake codeanalysis.ruleset = codeanalysis.ruleset GitVersion.yml = GitVersion.yml LICENSE.md = LICENSE.md README.md = README.md ReleaseNotes.md = ReleaseNotes.md EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "testing\Ocelot.Testing.csproj", "{D9DF2863-0608-4BBC-8D83-614E2894AF49}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.AcceptanceTests", "test\Ocelot.AcceptanceTests\Ocelot.AcceptanceTests.csproj", "{AA97C983-5A39-D35D-0274-08247BB08FAC}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Benchmarks", "test\Ocelot.Benchmarks\Ocelot.Benchmarks.csproj", "{075F01AD-8478-3463-2BB9-D04EE5C98321}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.ManualTest", "test\Ocelot.ManualTest\Ocelot.ManualTest.csproj", "{DE9F1F3D-7687-9248-E4D8-5684370E185F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.UnitTests", "test\Ocelot.UnitTests\Ocelot.UnitTests.csproj", "{47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot", "src\Ocelot\Ocelot.csproj", "{AB0A194F-36A2-D62B-51FD-44335BE12D1B}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Provider.Consul", "src\Ocelot.Provider.Consul\Ocelot.Provider.Consul.csproj", "{F38B5372-D141-3A0D-6FF8-30EFA93C7506}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Provider.Eureka", "src\Ocelot.Provider.Eureka\Ocelot.Provider.Eureka.csproj", "{3C77D0A0-173A-628F-FA3C-E113B9501E89}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Provider.Kubernetes", "src\Ocelot.Provider.Kubernetes\Ocelot.Provider.Kubernetes.csproj", "{1FD9E0B6-EC08-54E4-82F2-A215A388968D}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Provider.Polly", "src\Ocelot.Provider.Polly\Ocelot.Provider.Polly.csproj", "{1424441E-1143-0F05-1346-E90195CDE10E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x64.ActiveCfg = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x64.Build.0 = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x86.ActiveCfg = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Debug|x86.Build.0 = Debug|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|Any CPU.ActiveCfg = Release|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|Any CPU.Build.0 = Release|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x64.ActiveCfg = Release|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x64.Build.0 = Release|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x86.ActiveCfg = Release|Any CPU {D9DF2863-0608-4BBC-8D83-614E2894AF49}.Release|x86.Build.0 = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x64.ActiveCfg = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x64.Build.0 = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x86.ActiveCfg = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Debug|x86.Build.0 = Debug|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|Any CPU.Build.0 = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x64.ActiveCfg = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x64.Build.0 = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x86.ActiveCfg = Release|Any CPU {AA97C983-5A39-D35D-0274-08247BB08FAC}.Release|x86.Build.0 = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|Any CPU.Build.0 = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x64.ActiveCfg = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x64.Build.0 = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x86.ActiveCfg = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Debug|x86.Build.0 = Debug|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|Any CPU.ActiveCfg = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|Any CPU.Build.0 = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x64.ActiveCfg = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x64.Build.0 = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x86.ActiveCfg = Release|Any CPU {075F01AD-8478-3463-2BB9-D04EE5C98321}.Release|x86.Build.0 = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|Any CPU.Build.0 = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x64.ActiveCfg = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x64.Build.0 = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x86.ActiveCfg = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Debug|x86.Build.0 = Debug|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|Any CPU.ActiveCfg = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|Any CPU.Build.0 = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x64.ActiveCfg = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x64.Build.0 = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x86.ActiveCfg = Release|Any CPU {DE9F1F3D-7687-9248-E4D8-5684370E185F}.Release|x86.Build.0 = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x64.ActiveCfg = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x64.Build.0 = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x86.ActiveCfg = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Debug|x86.Build.0 = Debug|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|Any CPU.ActiveCfg = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|Any CPU.Build.0 = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x64.ActiveCfg = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x64.Build.0 = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x86.ActiveCfg = Release|Any CPU {47A6D609-F9EF-AE65-3904-5DBF15B0DEF1}.Release|x86.Build.0 = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x64.ActiveCfg = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x64.Build.0 = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x86.ActiveCfg = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Debug|x86.Build.0 = Debug|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|Any CPU.Build.0 = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x64.ActiveCfg = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x64.Build.0 = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x86.ActiveCfg = Release|Any CPU {AB0A194F-36A2-D62B-51FD-44335BE12D1B}.Release|x86.Build.0 = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|Any CPU.Build.0 = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x64.ActiveCfg = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x64.Build.0 = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x86.ActiveCfg = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Debug|x86.Build.0 = Debug|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|Any CPU.ActiveCfg = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|Any CPU.Build.0 = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x64.ActiveCfg = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x64.Build.0 = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x86.ActiveCfg = Release|Any CPU {F38B5372-D141-3A0D-6FF8-30EFA93C7506}.Release|x86.Build.0 = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x64.ActiveCfg = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x64.Build.0 = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x86.ActiveCfg = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Debug|x86.Build.0 = Debug|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|Any CPU.Build.0 = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x64.ActiveCfg = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x64.Build.0 = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x86.ActiveCfg = Release|Any CPU {3C77D0A0-173A-628F-FA3C-E113B9501E89}.Release|x86.Build.0 = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|Any CPU.Build.0 = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x64.ActiveCfg = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x64.Build.0 = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x86.ActiveCfg = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Debug|x86.Build.0 = Debug|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|Any CPU.ActiveCfg = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|Any CPU.Build.0 = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x64.ActiveCfg = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x64.Build.0 = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x86.ActiveCfg = Release|Any CPU {1FD9E0B6-EC08-54E4-82F2-A215A388968D}.Release|x86.Build.0 = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x64.ActiveCfg = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x64.Build.0 = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x86.ActiveCfg = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Debug|x86.Build.0 = Debug|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|Any CPU.ActiveCfg = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|Any CPU.Build.0 = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|x64.ActiveCfg = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|x64.Build.0 = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|x86.ActiveCfg = Release|Any CPU {1424441E-1143-0F05-1346-E90195CDE10E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} EndGlobalSection EndGlobal ================================================ FILE: Ocelot.slnx ================================================ ================================================ FILE: README.md ================================================ ![Ocelot Logo](https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/ocelot_logo.png) [![Release Status](https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml) [![Development Status](https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml) [![ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square)](https://app.readthedocs.org/projects/ocelot/builds/?version__slug=develop) [![coveralls](https://img.shields.io/coveralls/github/ThreeMammals/Ocelot/develop?label=coveralls&logo=coveralls&logoColor=white)](https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop) [![codecov](https://codecov.io/gh/ThreeMammals/Ocelot/branch/develop/graph/badge.svg)](https://codecov.io/gh/ThreeMammals/Ocelot) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/ThreeMammals/Ocelot/blob/main/LICENSE.md) [![NuGet](https://img.shields.io/nuget/v/Ocelot?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/Ocelot/ "Download Ocelot from NuGet.org") [![Downloads](https://img.shields.io/nuget/dt/Ocelot?logo=nuget&label=Downloads)](https://www.nuget.org/packages/Ocelot/ "Total Ocelot downloads from NuGet.org") [~docspassing]: https://img.shields.io/badge/Docs-passing-44CC11?style=flat-square [~docsfailing]: https://img.shields.io/badge/Docs-failing-red?style=flat-square ## About Ocelot is a .NET [API gateway](https://www.bing.com/search?q=API+gateway). This project is aimed at people using .NET running a microservices (service-oriented) architecture that needs a unified point of entry into their system. However, it will work with anything that speaks HTTP(S) and runs on any platform that [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/) supports. Ocelot consists of a series of ASP.NET Core [middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/) arranged in a specific order. Ocelot [custom middlewares](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/write) manipulate the `HttpRequest` object into a state specified by its configuration until it reaches a request builder middleware, where it creates a `HttpRequestMessage` object, which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is retrieved as the request goes back up the Ocelot pipeline. There is a piece of middleware that maps the `HttpResponseMessage` onto the `HttpResponse` object, and that is returned to the client. That is basically it, with a bunch of other features! ## Install Ocelot is designed to work with [ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/) and it targets `net9.0` [STS](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types) and `net8.0`, `net10.0` [LTS](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#release-types) target framework monikers ([TFMs](https://learn.microsoft.com/en-us/dotnet/standard/frameworks#supported-target-frameworks)). [^1] Install [Ocelot](https://www.nuget.org/packages/Ocelot) package and its dependencies using NuGet package manager: ```powershell Install-Package Ocelot ``` Or via the .NET CLI: ```shell dotnet add package Ocelot ``` > All versions are available [on NuGet](https://www.nuget.org/packages/Ocelot#versions-body-tab). ## Documentation - [RST-sources](https://github.com/ThreeMammals/Ocelot/tree/develop/docs): This includes the source code for the documentation (in reStructuredText format, .rst files), which is up to date for the current [development](https://github.com/ThreeMammals/Ocelot/tree/develop/). And the rendered HTML documentation is available [here](https://ocelot.readthedocs.io/en/develop/). - [Read the Docs](https://ocelot.readthedocs.io): This official website, in HTML format, contains a wealth of information and will be helpful if you want to understand the [features](#features) that Ocelot currently offers. The rendered HTML documentation, which is currently in [development](https://github.com/ThreeMammals/Ocelot/tree/develop/docs), is available [here](https://ocelot.readthedocs.io/en/develop/). - [Ask Ocelot Guru](https://gurubase.io/g/ocelot): It is an AI focused on Ocelot, designed to answer your questions. [^2] ## Features The primary features—[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) and [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html)—are always utilized by users, even in a minimal app setup, without customizations or extra configurations. Ocelot's capabilities are categorized into three main groups of features: *solid*, *hybrid*, and *feature-family* groups, which are explained below. - *Solid features* are unique to Ocelot. They do not contain subfeatures and are not related to other features. - *Hybrid features*, on the other hand, have multiple relationships with other features and can be part of other features. - *Feature families* are large groups that consist of multiple subfeatures. | Group | Features | |-------|----------| |Primary|[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html)| | Solid |[Caching](https://ocelot.readthedocs.io/en/latest/features/caching.html), [Delegating Handlers](https://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html), [Quality of Service](https://ocelot.readthedocs.io/en/latest/features/qualityofservice.html)[^3], [Rate Limiting](https://ocelot.readthedocs.io/en/latest/features/ratelimiting.html)| | Hybrid|[Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html), [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html)[^4], [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Dependency Injection](https://ocelot.readthedocs.io/en/latest/features/dependencyinjection.html), [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html)| |Family|[Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html), [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html), [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html), [Transformations](https://ocelot.readthedocs.io/en/latest/search.html?q=Transformation), [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html)[^5] | Feature groups are explained in the table below | Feature | Relationships and Notes | |---------|-------------------------| | [Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html) | [Administration](https://ocelot.readthedocs.io/en/latest/features/administration.html) heavily depends on [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), and [Administration API](https://ocelot.readthedocs.io/en/latest/features/administration.html#administration-api) methods are part of [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html), [Caching](https://ocelot.readthedocs.io/en/latest/features/caching.html), and [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) | | [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html)[^4] | [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html) relies on [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) | | [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html) | [Authentication](https://ocelot.readthedocs.io/en/latest/features/authentication.html) followed by [Authorization](https://ocelot.readthedocs.io/en/latest/features/authorization.html) | | [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) | [Configuration](https://ocelot.readthedocs.io/en/latest/features/configuration.html) depends on [Dependency Injection](https://ocelot.readthedocs.io/en/latest/features/dependencyinjection.html), including `GET`/`POST` operations via the [Administration REST API](https://ocelot.readthedocs.io/en/latest/features/administration.html#administration-api), a specialized [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) scheme/protocol, advanced [Middleware Injection](https://ocelot.readthedocs.io/en/latest/features/middlewareinjection.html), and [Metadata](https://ocelot.readthedocs.io/en/latest/features/metadata.html)-based extensions | | [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) | [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) offers specialized [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) and [Dynamic Routing](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing) modes but does not support [GraphQL](https://ocelot.readthedocs.io/en/latest/features/graphql.html)[^6] | | [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html) | [Load Balancer](https://ocelot.readthedocs.io/en/latest/features/loadbalancer.html) is a critical dependency for [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html) | | [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) | [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) includes [Error Handling](https://ocelot.readthedocs.io/en/latest/features/errorcodes.html) and [Tracing](https://ocelot.readthedocs.io/en/latest/features/tracing.html) | | [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html)[^5] | [Service Discovery](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html) with the following discovery providers: [Consul](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#consul), [Kubernetes](https://ocelot.readthedocs.io/en/latest/features/kubernetes.html), [Eureka](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#eureka), and [Service Fabric](https://ocelot.readthedocs.io/en/latest/features/servicefabric.html) | | [Transformations](https://ocelot.readthedocs.io/en/latest/search.html?q=Transformation) | They provide transformations for [Claims](https://ocelot.readthedocs.io/en/latest/features/claimstransformation.html), [Headers](https://ocelot.readthedocs.io/en/latest/features/headerstransformation.html), and [Method](https://ocelot.readthedocs.io/en/latest/features/methodtransformation.html) | > Ocelot customizations can be configured using [Metadata](https://ocelot.readthedocs.io/en/latest/features/metadata.html), developed with [Delegating Handlers](https://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html), and in advanced scenarios, they can be developed and then configured with [Middleware Injection](https://ocelot.readthedocs.io/en/latest/features/middlewareinjection.html). For further details, refer to the [Documentation](#documentation). ## Contributing You can see what we are working on in the [backlog](https://github.com/ThreeMammals/Ocelot/issues). We love to receive contributions from the community, so please keep them coming. Pull requests, issues, and commentary welcome! octocat Please complete the relevant [template](https://github.com/ThreeMammals/Ocelot/tree/main/.github) for [issues](https://github.com/ThreeMammals/Ocelot/blob/main/.github/ISSUE_TEMPLATE.md) and [pull requests](https://github.com/ThreeMammals/Ocelot/blob/main/.github/PULL_REQUEST_TEMPLATE.md). Sometimes it's worth getting in touch with us to [discuss](https://github.com/ThreeMammals/Ocelot/discussions) changes before doing any work in case this is something we are already doing or it might not make sense. We can also give advice on the easiest way to do things octocat Finally, we mark all existing issues as [![label: help wanted][~helpwanted]](https://github.com/ThreeMammals/Ocelot/labels/help%20wanted) [![label: small effort][~smalleffort]](https://github.com/ThreeMammals/Ocelot/labels/small%20effort) [![label: medium effort][~mediumeffort]](https://github.com/ThreeMammals/Ocelot/labels/medium%20effort) [![label: large effort][~largeeffort]](https://github.com/ThreeMammals/Ocelot/labels/large%20effort).[^7] If you want to contribute for the first time, we suggest looking at a [![label: help wanted][~helpwanted]](https://github.com/ThreeMammals/Ocelot/labels/help%20wanted) [![label: small effort][~smalleffort]](https://github.com/ThreeMammals/Ocelot/labels/small%20effort) [![label: good first issue][~goodfirstissue]](https://github.com/ThreeMammals/Ocelot/labels/good%20first%20issue) octocat [~helpwanted]: https://img.shields.io/badge/-help%20wanted-128A0C.svg [~smalleffort]: https://img.shields.io/badge/-small%20effort-fef2c0.svg [~mediumeffort]: https://img.shields.io/badge/-medium%20effort-e0f42c.svg [~largeeffort]: https://img.shields.io/badge/-large%20effort-10526b.svg [~goodfirstissue]: https://img.shields.io/badge/-good%20first%20issue-ffc4d8.svg ### Notes [^1]: Starting with version [21](https://github.com/ThreeMammals/Ocelot/releases/tag/21.0.0) and higher, the solution's code base supports [Multitargeting](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-multitargeting-overview) as SDK-style projects. It should be easier for teams to migrate to the currently supported [.NET 8, 9 and 10](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core#lifecycle) frameworks. Also, new features will be available for all .NET SDKs that we support via multitargeting. Find out more here: [Target frameworks in SDK-style projects](https://learn.microsoft.com/en-us/dotnet/standard/frameworks) [^2]: [Ocelot Guru](https://gurubase.io/g/ocelot) is an unofficial tool to get answers regarding Ocelot: please consider it an advanced search tool. Thus, we have an official [Questions & Answers](https://github.com/ThreeMammals/Ocelot/discussions/categories/q-a) category in the [Discussions](https://github.com/ThreeMammals/Ocelot/discussions) space. [^3]: Retry policies only via [Polly](https://github.com//App-vNext/Polly) library referenced within the [Ocelot.Provider.Polly](https://www.nuget.org/packages/Ocelot.Provider.Polly) extension package. [^4]: Previously, the [Aggregation](https://ocelot.readthedocs.io/en/latest/features/aggregation.html) feature was called [Request Aggregation](https://ocelot.readthedocs.io/en/23.4.3/features/requestaggregation.html) in versions [23.4.3](https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.3) and earlier. Internally, within the Ocelot team, this feature is referred to as [Multiplexer](https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/Multiplexer). [^5]: Ocelot supports the following service discovery providers: (**1**) [Consul](https://www.consul.io) through the [Ocelot.Provider.Consul](https://www.nuget.org/packages/Ocelot.Provider.Consul) extension package, (**2**) [Kubernetes](https://kubernetes.io) via the [Ocelot.Provider.Kubernetes](https://www.nuget.org/packages/Ocelot.Provider.Kubernetes) extension package, and (**3**) [Netflix Eureka](https://spring.io/projects/spring-cloud-netflix), which utilizes the [Steeltoe.Discovery.Eureka](https://www.nuget.org/packages/Steeltoe.Discovery.Eureka) package referenced within the [Ocelot.Provider.Eureka](https://www.nuget.org/packages/Ocelot.Provider.Eureka) extension package. Additionally, Ocelot supports (**4**) Azure [Service Fabric](https://azure.microsoft.com/en-us/products/service-fabric/) for service discovery, along with special modes such as [Dynamic Routing](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#dynamic-routing) and [Custom Providers](https://ocelot.readthedocs.io/en/latest/features/servicediscovery.html#custom-providers). [^6]: Ocelot does not directly support [GraphQL](https://graphql.org/). Developers can easily integrate the [GraphQL for .NET](https://github.com/graphql-dotnet/graphql-dotnet) library. [^7]: See all [labels](https://github.com/ThreeMammals/Ocelot/issues/labels) for the repository, which are useful for searching and filtering. ================================================ FILE: ReleaseNotes.md ================================================ ## Pre-release 2 for [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) SDK (version [{0}](https://www.nuget.org/packages/Ocelot/{0})) > Milestone: [.NET 10](https://github.com/ThreeMammals/Ocelot/milestone/13) This is **Pre-release 2** for the [.NET 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) SDK. Version [{0}](https://www.nuget.org/packages/Ocelot/{0}) includes upgraded solutions and [NuGet packages](https://www.nuget.org/profiles/ThreeMammals) based on .NET SDK [10.0.201](https://dotnet.microsoft.com/en-us/download/dotnet/10.0), released on March 12, 2026. For more details about SDK [10.0.201](https://dotnet.microsoft.com/en-us/download/dotnet/10.0), see the [Release notes](https://github.com/dotnet/core/blob/main/release-notes/10.0/10.0.5/10.0.5.md). Development teams can start migrating their Ocelot-based projects using this [Beta 2](https://www.nuget.org/packages/Ocelot/{0}) release to upgrade to the .NET 10 SDK with Long-Term Support (LTS). ================================================ FILE: build.cake ================================================ #tool dotnet:?package=GitVersion.Tool&version=6.6.2 #tool nuget:?package=ReportGenerator&version=5.5.4 #addin nuget:?package=Cake.Http #addin nuget:?package=Newtonsoft.Json&version=13.0.4 // Switch to a MS lib! #addin nuget:?package=System.Text.Encodings.Web&version=10.0.5 #r "Spectre.Console" using Spectre.Console; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using _File_ = System.IO.File; using _Directory_ = System.IO.Directory; bool IsTechnicalRelease = false; const string Release = "Release"; // task name, target, and Release config name const string PullRequest = "PullRequest"; // task name, target, and PullRequest config name const string LatestFramework = "LatestFramework"; // task name, target, and LatestFramework config name const string AllTFMs = "net8.0;net9.0;net10.0"; const string LatestTFM = "net10.0"; string NL = Environment.NewLine; // Create a CultureInfo object for UK English CultureInfo ukCulture = new("en-GB"); CultureInfo.DefaultThreadCurrentCulture = ukCulture; CultureInfo.DefaultThreadCurrentUICulture = ukCulture; Information("Current Culture: " + CultureInfo.CurrentCulture); Information("Current UI Culture: " + CultureInfo.CurrentUICulture); // Display culture properties Information("Culture Name: " + ukCulture.Name); // en-GB Information("Display Name: " + ukCulture.DisplayName); // English (United Kingdom) Information("English Name: " + ukCulture.EnglishName); // English (United Kingdom) Information("Native Name: " + ukCulture.NativeName); // English (United Kingdom) Information("Two-letter ISO Language Name: " + ukCulture.TwoLetterISOLanguageName); // en Information("Three-letter ISO Language Name: " + ukCulture.ThreeLetterISOLanguageName); // eng Information("Region ISO Code: " + new RegionInfo(ukCulture.Name).TwoLetterISORegionName); // GB // Example: format a date and currency in UK style DateTime now = DateTime.Now; decimal amount = 12345.67m; Information("Date (UK format): " + now.ToString("D", ukCulture)); Information("Currency (UK format): " + amount.ToString("C", ukCulture)); var compileConfig = Argument("configuration", Release); // compile var artifactsDir = Directory("artifacts"); // build artifacts // unit testing var artifactsForUnitTestsDir = artifactsDir + Directory("UnitTests"); var unitTestAssemblies = @"./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj"; // acceptance testing var artifactsForAcceptanceTestsDir = artifactsDir + Directory("AcceptanceTests"); var acceptanceTestAssemblies = @"./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj"; // benchmark testing var artifactsForBenchmarkTestsDir = artifactsDir + Directory("BenchmarkTests"); var benchmarkTestAssemblies = @"./test/Ocelot.Benchmarks"; // packaging var packagesDir = artifactsDir + Directory("Packages"); var artifactsFile = packagesDir + File("artifacts.txt"); var releaseNotesFile = packagesDir + File("ReleaseNotes.md"); var releaseNotes = new List(); // internal build variables - don't change these. string committedVersion = "0.0.0-dev"; GitVersion versioning = null; var target = Argument("target", "Default"); var slnFile = "./Ocelot.slnx"; Information($"{NL}Target: {target}"); Information($"Build: {compileConfig}"); Information($"Solution: {slnFile}"); TaskTeardown(context => { AnsiConsole.Markup($"[green]DONE[/] {context.Task.Name}" + NL); }); Task("Default") .IsDependentOn("Build"); Task("Build") .IsDependentOn("Tests"); Task("LatestFramework") .IsDependentOn("Tests"); Task("PullRequest") .IsDependentOn("Tests"); Task("ReleaseNotes") .IsDependentOn("CreateReleaseNotes"); Task("Tests") .IsDependentOn("UnitTests") .IsDependentOn("AcceptanceTests"); Task("Release") .IsDependentOn("Build") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("CreateArtifacts") .IsDependentOn("PublishGitHubRelease") .IsDependentOn("PublishToNuget"); Task("Restore") .Does(() => { var settings = new DotNetRestoreSettings { LockedMode = true, // equivalent to --locked-mode // UseLockFile = true, // equivalent to --use-lock-file // Sources = new[] { "https://api.nuget.org/v3/index.json" } }; DotNetRestore(slnFile, settings); }); Task("Compile") .IsDependentOn("Clean") .IsDependentOn("Version") .IsDependentOn("Restore") .Does(() => { PreprocessReadMe(); Information("Branch: " + GetBranchName()); Information("Build: " + compileConfig); Information("Solution: " + slnFile); var settings = new DotNetBuildSettings { Configuration = compileConfig, NoRestore = true, }; if (target == LatestFramework || target == PullRequest) { settings.Framework = LatestTFM; // build using .NET 10 SDK only } string frameworkInfo = string.IsNullOrEmpty(settings.Framework) ? AllTFMs : settings.Framework; Information($"Settings {nameof(DotNetBuildSettings.Framework)}: {frameworkInfo}"); Information($"Settings {nameof(DotNetBuildSettings.Configuration)}: {settings.Configuration}"); DotNetBuild(slnFile, settings); }); Task("Clean") .Does(() => { if (DirectoryExists(artifactsDir)) { DeleteDirectory(artifactsDir, new DeleteDirectorySettings { Recursive = true, Force = true }); } CreateDirectory(artifactsDir); }); Task("Version") .Does(() => { versioning = GetNuGetVersionForCommit(); versioning.NuGetVersion ??= versioning.SemVer; if (target == Release && IsRunningInCICD() && IsMainBranch() && versioning.SemVer.Contains("-")) // dash -> suffix in version versioning.NuGetVersion = versioning.MajorMinorPatch; // when releasing from main branch the tag should not contain suffix after dash char Information("#########################"); Information("# SemVer Information"); Information("#========================"); Information($"# {nameof(versioning.NuGetVersion)}: {versioning.NuGetVersion}"); Information($"# {nameof(versioning.BranchName)}: {versioning.BranchName}"); Information($"# {nameof(versioning.MajorMinorPatch)}: {versioning.MajorMinorPatch}"); Information($"# {nameof(versioning.SemVer)}: {versioning.SemVer}"); Information($"# {nameof(versioning.InformationalVersion)}: {versioning.InformationalVersion}"); Information("#########################"); Information($"Persisting version number... {nameof(versioning.NuGetVersion)} -> {versioning.NuGetVersion}"); PersistVersion(committedVersion, versioning.NuGetVersion); }); Task("GitLogUniqContributors") .Does(() => { var command = "log --format=\"%aN|%aE\" "; // command += IsRunningInCICD() ? "| sort | uniq" : // IsRunningInPowershell() ? "| Sort-Object -Unique" : "| sort | uniq"; List output = GitHelper(command); output.Sort(); List contributors = output.Distinct().ToList(); contributors.Sort(); Information($"Detected {contributors.Count} unique contributors:"); Information(string.Join(NL, contributors)); // TODO Search example in bash: curl -L -H "X-GitHub-Api-Version: 2022-11-28" "https://api.github.com/search/users?q=Chris+Swinchatt" Information(NL + "Unicode test: 1) Raynald Messié; 2) 彭伟 pengweiqhca"); AnsiConsole.Markup("Unicode test: 1) Raynald Messié; 2) 彭伟 pengweiqhca" + NL); // Powershell life hack: $OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding // https://stackoverflow.com/questions/40098771/changing-powershells-default-output-encoding-to-utf-8 // https://stackoverflow.com/questions/49476326/displaying-unicode-in-powershell/49481797#49481797 // https://stackoverflow.com/questions/57131654/using-utf-8-encoding-chcp-65001-in-command-prompt-windows-powershell-window/57134096#57134096 }); Task("CreateReleaseNotes") .IsDependentOn("Version") //.IsDependentOn("GitLogUniqContributors") .Does(() => { Information($"Generating release notes at {releaseNotesFile}"); var lastReleaseTags = GitHelper("describe --tags --abbrev=0 --exclude net*"); var lastRelease = lastReleaseTags.First(t => !t.StartsWith("net")); // skip 'net*-vX.Y.Z' tag and take 'major.minor.build' var releaseVersion = versioning.NuGetVersion; // Read main header from Git file, substitute version in header, and add content further... Information("{0} New release tag is " + releaseVersion); Information("{1} Last release tag is " + lastRelease); var body = _File_.ReadAllText("./ReleaseNotes.md", System.Text.Encoding.UTF8); var releaseHeader = string.Format(body, releaseVersion, lastRelease); releaseNotes = new List { releaseHeader }; if (IsTechnicalRelease) { WriteReleaseNotes(); return; } const bool debugUserEmail = false; var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary --email {lastRelease}..HEAD") .ToList(); var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)[\s\t]+<(?'email'.*)>.*$"); static SummaryItem CreateSummaryItem(System.Text.RegularExpressions.Match m) => new() { Commits = int.Parse(m.Groups["commits"]?.Value ?? "0"), Author = m.Groups["author"]?.Value?.Trim() ?? string.Empty, Email = m.Groups["email"]?.Value?.Trim() ?? string.Empty, }; var summary = shortlogSummary .Where(x => re.IsMatch(x)) .Select(x => re.Match(x)) .Select(CreateSummaryItem) .ToList(); // Starring aka Release Influencers var starring = new List(); string CreateStars(int count, string name) { var contributor = summary.Find(x => x.Author.Equals(name)); var stars = string.Join(string.Empty, Enumerable.Repeat(":star:", count)); var emailInfo = debugUserEmail ? ", " + contributor.Email : string.Empty; return $"{stars} {contributor.Author}{emailInfo}"; } Information("------==< Old Starring >==------"); foreach (var contributor in summary) { starring.Add(CreateStars(contributor.Commits, contributor.Author)); } Information(string.Join(NL, starring)); var commitsGrouping = summary .GroupBy(x => x.Commits) .Select(CreateCommitsGroupingItem) .OrderByDescending(x => x.Commits) .ToList(); starring = IterateCommits(commitsGrouping, breaker: log => false, // don't break, so iterate all groups (summary) byCommits: (log, group) => CreateStars(group.Commits, group.Authors.First()), byFiles: (log, group, fGroup) => CreateStars(group.Commits, fGroup.Contributors.First().Contributor), byInsertions: (log, group, fGroup, insGroup) => CreateStars(group.Commits, insGroup.Contributors.First().Contributor), byDeletions: (log, group, fGroup, insGroup, contributor) => CreateStars(group.Commits, contributor.Contributor)); Information("------==< New Starring >==------"); Information(string.Join(NL, starring)); // Honoring aka Top Contributors var coreTeamNames = new List { "Raman Maksimchuk", "Raynald Messié", "Guillaume Gnaegi" }; // Ocelot Core team members should not be in Top 3 Chart var coreTeamEmails = new List { "dotnet044@gmail.com", "redbird_project@yahoo.fr", "58469901+ggnaegi@users.noreply.github.com" }; static CommitsGroupingItem CreateCommitsGroupingItem(IGrouping g) => new() { Commits = g.Key, Count = g.Count(), Authors = g.Select(x => x.Author).ToArray(), }; commitsGrouping = summary .Where(x => !coreTeamNames.Contains(x.Author) && !coreTeamEmails.Contains(x.Email)) // filter out Ocelot Core team members .GroupBy(x => x.Commits) .Select(CreateCommitsGroupingItem) .OrderByDescending(x => x.Commits) .ToList(); var topContributors = IterateCommits(commitsGrouping, breaker: log => false, // (log.Count >= 3), // going to create Top 3 byCommits: (log, group) => { var place = Place(log.Count); var author = group.Authors.First(); return Honor(place, author, group.Commits); }, byFiles: (log, group, fGroup) => { var place = Place(log.Count); var contributor = fGroup.Contributors.First(); return HonorForFiles(place, contributor.Contributor, group.Commits, contributor.Files); }, byInsertions: (log, group, fGroup, insGroup) => { var place = Place(log.Count); var contributor = insGroup.Contributors.First(); return HonorForInsertions(place, contributor.Contributor, group.Commits, contributor.Files, contributor.Insertions); }, byDeletions: (log, group, fGroup, insGroup, contributor) => { var place = Place(log.Count); return HonorForDeletions(place, contributor.Contributor, group.Commits, contributor.Files, contributor.Insertions, contributor.Deletions); }); Information("------==< TOP Contributors >==------"); Information(string.Join(NL, topContributors)); // local helpers static string Place(int i) => ++i == 1 ? "1st" : i == 2 ? "2nd" : i == 3 ? "3rd" : $"{i}th"; static string Plural(int n) => n == 1 ? "" : "s"; static string Honor(string place, string author, int commits, string suffix = null) => $"{place[0]}{place[1..]} :{place}_place_medal: goes to **{author}** for delivering **{commits}** feature{Plural(commits)} {suffix ?? ""}"; static string HonorForFiles(string place, string author, int commits, int files, string suffix = null) => Honor(place, author, commits, $"in **{files}** file{Plural(files)} changed {suffix ?? ""}"); static string HonorForInsertions(string place, string author, int commits, int files, int insertions, string suffix = null) => HonorForFiles(place, author, commits, files, $"with **{insertions}** insertion{Plural(insertions)} {suffix ?? ""}"); static string HonorForDeletions(string place, string author, int commits, int files, int insertions, int deletions) => HonorForInsertions(place, author, commits, files, insertions, $"and **{deletions}** deletion{Plural(deletions)}"); List IterateCommits(List commitsGrouping, Predicate> breaker, Func, CommitsGroupingItem, string> byCommits, Func, CommitsGroupingItem, FilesGroupingItem, string> byFiles, Func, CommitsGroupingItem, FilesGroupingItem, InsertionsGroupingItem, string> byInsertions, Func, CommitsGroupingItem, FilesGroupingItem, InsertionsGroupingItem, FilesChangedItem, string> byDeletions) { var log = new List(); foreach (var group in commitsGrouping) { if (breaker.Invoke(log)) break; // (log.Count >= top3) if (group.Count == 1) { log.Add(byCommits.Invoke(log, group)); } else // multiple candidates with the same number of commits, so, group by files changed { var statistics = new List(); var shortstatRegex = new Regex(@"^\s*(?'files'\d+)\s+files?\s+changed(?'ins',\s+(?'insertions'\d+)\s+insertions?\(\+\))?(?'del',\s+(?'deletions'\d+)\s+deletions?\(\-\))?\s*$"); static FilesChangedItem CreateFilesChangedItem(System.Text.RegularExpressions.Match m) { FilesChangedItem item = new(); if (int.TryParse(m.Groups["files"]?.Value ?? "0", out int files)) item.Files = files; else item.Files = 0; if (int.TryParse(m.Groups["insertions"]?.Value ?? "0", out int insertions)) item.Insertions = insertions; else item.Insertions = 0; if (int.TryParse(m.Groups["deletions"]?.Value ?? "0", out int deletions)) item.Deletions = deletions; else item.Deletions = 0; return item; } foreach (var author in group.Authors) // Collect statistics from git log & shortlog { if (!statistics.Exists(s => s.Contributor == author)) { var shortstat = GitHelper($"log --no-merges --author=\"{author}\" --shortstat --pretty=oneline {lastRelease}..HEAD"); var data = shortstat .Where(x => shortstatRegex.IsMatch(x)) .Select(x => shortstatRegex.Match(x)) .Select(CreateFilesChangedItem) .ToList(); statistics.Add(new FilesChangedItem(author, data.Sum(x => x.Files), data.Sum(x => x.Insertions), data.Sum(x => x.Deletions))); } } var filesGrouping = statistics .GroupBy(x => x.Files) .Select(g => new FilesGroupingItem { Files = g.Key, Count = g.Count(), Contributors = g.SelectMany(x => statistics.Where(s => s.Contributor == x.Contributor && s.Files == g.Key)).ToArray(), }) .OrderByDescending(x => x.Files) .ToList(); foreach (var fGroup in filesGrouping) { if (breaker.Invoke(log)) break; if (fGroup.Count == 1) { log.Add(byFiles.Invoke(log, group, fGroup)); } else // multiple candidates with the same number of commits, with the same number of changed files, so, group by additions (insertions) { var insertionsGrouping = fGroup.Contributors .GroupBy(x => x.Insertions) .Select(g => new InsertionsGroupingItem { Insertions = g.Key, Count = g.Count(), Contributors = g.SelectMany(x => fGroup.Contributors.Where(s => s.Contributor == x.Contributor && s.Insertions == g.Key)).ToArray(), }) .OrderByDescending(x => x.Insertions) .ToList(); foreach (var insGroup in insertionsGrouping) { if (breaker.Invoke(log)) break; if (insGroup.Count == 1) { log.Add(byInsertions.Invoke(log, group, fGroup, insGroup)); } else // multiple candidates with the same number of commits, with the same number of changed files, with the same number of insertions, so, order desc by deletions { foreach (var contributor in insGroup.Contributors.OrderByDescending(x => x.Deletions)) { if (breaker.Invoke(log)) break; log.Add(byDeletions.Invoke(log, group, fGroup, insGroup, contributor)); } } } } } } } return log; } // END of IterateCommits // releaseNotes.Add("### Honoring :medal_sports: aka Top Contributors :clap:"); // releaseNotes.AddRange(topContributors.Take(3)); // Top 3 only, disabled 'breaker' logic // releaseNotes.Add(""); // releaseNotes.Add("### Starring :star: aka Release Influencers :bowtie:"); // releaseNotes.AddRange(starring); // releaseNotes.Add(""); // releaseNotes.Add($"### Features in Release {releaseVersion}"); // releaseNotes.Add(""); // releaseNotes.Add("
Logbook"); // releaseNotes.Add(""); // var commitsHistory = GitHelper($"log --no-merges --date=format:\"%A, %B %d at %H:%M\" --pretty=format:\"- %h by **%aN** on %ad →%n %s\" {lastRelease}..HEAD"); // releaseNotes.AddRange(commitsHistory); // releaseNotes.Add("
"); // releaseNotes.Add(""); WriteReleaseNotes(); }); struct SummaryItem { public int Commits; public string Author; public string Email; } struct CommitsGroupingItem { public int Commits; public int Count; public string[] Authors; } struct FilesChangedItem { public string Contributor; public int Files; public int Insertions; public int Deletions; public FilesChangedItem(string author, int files, int insertions, int deletions) { Contributor = author; Files = files; Insertions = insertions; Deletions = deletions; } } struct FilesGroupingItem { public int Files; public int Count; public FilesChangedItem[] Contributors; } struct InsertionsGroupingItem { public int Insertions; public int Count; public FilesChangedItem[] Contributors; } private List GitHelper(string command) { IEnumerable output; var exitCode = StartProcess( "git", new ProcessSettings { Arguments = command, RedirectStandardOutput = true }, out output); if (exitCode != 0) throw new Exception("Failed to execute Git command: " + command); return output.ToList(); } private void WriteReleaseNotes() { Information($"RUN {nameof(WriteReleaseNotes)} ..."); EnsureDirectoryExists(packagesDir); _File_.WriteAllLines(releaseNotesFile, releaseNotes, Encoding.UTF8); var content = _File_.ReadAllText(releaseNotesFile, Encoding.UTF8); if (string.IsNullOrEmpty(content)) { _File_.WriteAllText(releaseNotesFile, "No commits since last release", System.Text.Encoding.UTF8); } Information("Release notes are >>>{0}<<<", NL + content); } private List GetTFMs() { var tfms = AllTFMs.Split(';').ToList(); if (target == LatestFramework || target == "UnitTests" || target == Release || target == PullRequest) { tfms.Clear(); tfms.Add(LatestTFM); } return tfms; } Task("UnitTests") .IsDependentOn("Compile") .Does(() => { var verbosity = IsRunningInCICD() ? "minimal" : "normal"; // Sequential processing as an emulation of Visual Studio Test Explorer foreach (string tfm in GetTFMs()) { var settings = new DotNetTestSettings { Configuration = compileConfig, ResultsDirectory = artifactsForUnitTestsDir, ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") .Append("--collect:\"XPlat Code Coverage\"") // this create the code coverage report .Append("--settings coverlet.runsettings") // exclude Ocelot.Testing assembly from coverage .Append("--verbosity:" + verbosity) .Append("--consoleLoggerParameters:ErrorsOnly"), Framework = tfm, }; Information($"Settings {nameof(settings.Framework)}: {settings.Framework}"); EnsureDirectoryExists(artifactsForUnitTestsDir); DotNetTest(unitTestAssemblies, settings); // sequential testing } Information("ArtifactsForUnitTestsDir = " + artifactsForUnitTestsDir); var coverageSummaryFile = GetSubDirectories(artifactsForUnitTestsDir) .First() .CombineWithFilePath(File("coverage.cobertura.xml")); Information("CoverageSummaryFile = " + coverageSummaryFile); GenerateReport(coverageSummaryFile); Information("##############################"); Information("# Code coverage"); Information("#============================="); // TODO Implement reporting to the Action Run summary as an attachment or artifact const string CoverallsRepo = "https://coveralls.io/github/ThreeMammals/Ocelot"; Information($"# There is dedicated Coveralls step of GH Action workflows. So, we won't publish the coverage report to coveralls.io"); // Apply code coverage threshold const double MinCodeCoverage = 0.93D; // consider definition of an env var in GitHub Environment vars var lineCoverage = XmlPeek(coverageSummaryFile, "//coverage/@line-rate"); var branchCoverage = XmlPeek(coverageSummaryFile, "//coverage/@branch-rate"); Information("# Line Coverage: " + lineCoverage); Information("# Branch Coverage: " + branchCoverage); if (double.Parse(lineCoverage) < MinCodeCoverage) { var whereToCheck = !IsRunningInCICD() ? CoverallsRepo : artifactsForUnitTestsDir; var msg = $"# Code coverage fell below the threshold of {MinCodeCoverage * 100}%. You can find the code coverage report at {whereToCheck}"; Warning(msg); // throw new Exception(msg); // fail the building job step in GitHub Actions }; Information("##############################"); }); Task("AcceptanceTests") .IsDependentOn("Compile") .Does(() => { var verbosity = IsRunningInCICD() ? "minimal" : "normal"; if (IsRunningInCICD() && target == Release) { Warning("We are rolling out a release through the CI/CD pipeline, so we won't be running acceptance tests this time!"); return; } // Sequential processing as an emulation of Visual Studio Test Explorer foreach (string tfm in GetTFMs()) { var settings = new DotNetTestSettings { Configuration = compileConfig, ArgumentCustomization = args => args .Append("--no-restore") .Append("--no-build") .Append("--verbosity:" + verbosity), Framework = tfm, }; Information($"Settings {nameof(settings.Framework)}: {settings.Framework}"); EnsureDirectoryExists(artifactsForAcceptanceTestsDir); DotNetTest(acceptanceTestAssemblies, settings); } }); Task("CreateArtifacts") .IsDependentOn("CreateReleaseNotes") .IsDependentOn("Compile") .Does(() => { WriteReleaseNotes(); _File_.AppendAllLines(artifactsFile, new[] { "ReleaseNotes.md" }); if (!IsTechnicalRelease) { CopyFiles("./**/Release/Ocelot.*.{nupkg,snupkg}", packagesDir); var projectFiles = GetFiles("./**/Release/Ocelot.*.{nupkg,snupkg}") .OrderBy(f => f.GetFilenameWithoutExtension().ToString()) .ThenBy(f => f.GetExtension().ToString()) // .nupkg first .ToList(); foreach(var projectFile in projectFiles) { _File_.AppendAllLines(artifactsFile, new[] { projectFile.GetFilename().FullPath }); } } var artifacts = _File_.ReadAllLines(artifactsFile) .Distinct(); Information($"Listing all {nameof(artifacts)}..."); foreach (var artifact in artifacts) { var codePackage = packagesDir + File(artifact); if (FileExists(codePackage)) { Information("Created package " + codePackage); } else { Information("Package does not exist: " + codePackage); } } }); Task("PublishGitHubRelease") .IsDependentOn("CreateArtifacts") .Does(() => { if (!IsRunningInCICD()) { Warning("We are not running on the CI/CD so we won't publish a GitHub release"); return; } dynamic release = CreateGitHubRelease(); var path = packagesDir.ToString() + @"/**/*Ocelot.*"; // filter out artifacts.txt and ReleaseNotes.md var files = GetFiles(path).ToList(); foreach (var file in files) { UploadFileToGitHubRelease(release, file); } CompleteGitHubRelease(release); }); Task("EnsureStableReleaseRequirements") .Does(() => { Information("Check if stable release..."); if (!IsRunningInCICD()) { throw new Exception("Stable release should happen via CI/CD"); } Information("Release is stable..."); }); Task("DownloadGitHubReleaseArtifacts") .Does(async () => { try { // hack to let GitHub catch up, todo - refactor to poll System.Threading.Thread.Sleep(5000); EnsureDirectoryExists(packagesDir); var releaseUrl = "https://api.github.com/repos/ThreeMammals/ocelot/releases/tags/" + versioning.NuGetVersion; var releaseInfo = await GetResourceAsync(releaseUrl); var assets_url = Newtonsoft.Json.Linq.JObject.Parse(releaseInfo) .Value("assets_url"); var assets = await GetResourceAsync(assets_url); foreach(var asset in Newtonsoft.Json.JsonConvert.DeserializeObject(assets)) { var file = packagesDir + File(asset.Value("name")); DownloadFile(asset.Value("browser_download_url"), file); } } catch(Exception exception) { Information("There was an exception " + exception); throw; } }); Task("PublishToNuget") .IsDependentOn("DownloadGitHubReleaseArtifacts") .Does(() => { if (IsTechnicalRelease) { Information("Skipping of publishing to NuGet because of technical release..."); return; } if (!IsRunningInCICD()) { Warning("We are not running on the CI/CD so we won't publish NuGet packages."); //return; } var nugetFeedStableKey = EnvironmentVariable("OCELOT_NUGET_API_KEY_2025"); var nugetFeedStableUploadUrl = "https://www.nuget.org/api/v2/package"; var nugetFeedStableSymbolsUploadUrl = "https://www.nuget.org/api/v2/package"; PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); }); Task("Void").Does(() => {}); RunTarget(target); private void PreprocessReadMe() { const string READMEmd = "./README.md"; const string RTD_NuGet_Valid_Domain = "[ReadTheDocs][~docspassing]"; const string RTD_Version_Latest = "[ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=latest&style=flat-square)"; const string RTD_Version_Develop = "[ReadTheDocs](https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square)"; Information($"Processing {READMEmd} ..."); var body = _File_.ReadAllText(READMEmd, System.Text.Encoding.UTF8); var RTD_IsReplaced = false; if (body.Contains(RTD_Version_Latest)) { Information($" {READMEmd}: Detected ReadTheDocs LATEST version marker -> {RTD_Version_Latest}"); body = body.Replace(RTD_Version_Latest, RTD_NuGet_Valid_Domain); RTD_IsReplaced = true; } if (body.Contains(RTD_Version_Develop)) { Information($" {READMEmd}: Detected ReadTheDocs DEVELOP version marker -> {RTD_Version_Develop}"); body = body.Replace(RTD_Version_Develop, RTD_NuGet_Valid_Domain); RTD_IsReplaced = true; } if (RTD_IsReplaced) { Information($" {READMEmd}: ReadTheDocs badge has been replaced with -> {RTD_NuGet_Valid_Domain}"); } const string IMG_Octocat_HTML = "\"octocat\""; const string IMG_NuGet_Valid_MD = "![octocat](https://raw.githubusercontent.com/ThreeMammals/Ocelot/refs/heads/assets/images/octocat-25px.png)"; if (body.Contains(IMG_Octocat_HTML)) { Information($" {READMEmd}: Detected Octocat HTML IMG-tag -> " + IMG_Octocat_HTML); body = body.Replace(IMG_Octocat_HTML, IMG_NuGet_Valid_MD); Information($" {READMEmd}: Octocat HTML IMG-tag has been replaced with -> " + IMG_NuGet_Valid_MD); } Information($" {READMEmd}: Writing the body of the {READMEmd}..."); _File_.WriteAllText(READMEmd, body, System.Text.Encoding.UTF8); Information($"DONE Processing {READMEmd}{NL}"); } private void GenerateReport(Cake.Core.IO.FilePath coverageSummaryFile) { var dir = _Directory_.GetCurrentDirectory(); Information("GenerateReport: Current directory: " + dir); var reportSettings = new ProcessArgumentBuilder(); reportSettings.Append($"-targetdir:" + $"{dir}/{artifactsForUnitTestsDir}"); reportSettings.Append($"-reports:" + coverageSummaryFile); reportSettings.Append($"-filefilters:-*.g.cs"); // silence warnings for source-generated files (e.g. RegexGenerator.g.cs) that are deleted after build Information($"GenerateReport: Resolving net10.0/ReportGenerator.dll ..."); var toolpath = Context.Tools.Resolve("net10.0/ReportGenerator.dll"); Information($"GenerateReport: Tool Path: {toolpath.ToString()}" + NL); DotNetExecute(toolpath, reportSettings); } /// Gets unique nuget version for this commit private GitVersion GetNuGetVersionForCommit() { GitVersion(new GitVersionSettings{ UpdateAssemblyInfo = false, OutputType = GitVersionOutput.BuildServer, Verbosity = IsRunningInCICD() ? GitVersionVerbosity.Minimal : GitVersionVerbosity.Normal, }); return GitVersion(new GitVersionSettings{ OutputType = GitVersionOutput.Json }); } /// Updates project version in all of our projects private void PersistVersion(string committedVersion, string newVersion) { Information(string.Format("We'll search all csproj files for {0} and replace with {1}...", committedVersion, newVersion)); var projectFiles = GetFiles("./**/*.csproj") .Where(f => !f.FullPath.Contains("Ocelot.Samples.")) .ToList(); foreach(var projectFile in projectFiles) { var file = projectFile.ToString(); Information(string.Format("Updating {0}...", file)); var updatedProjectFile = _File_.ReadAllText(file, System.Text.Encoding.UTF8) .Replace(committedVersion, newVersion); _File_.WriteAllText(file, updatedProjectFile, System.Text.Encoding.UTF8); } } // Publishes code and symbols packages to nuget feed, based on contents of artifacts file private void PublishPackages(ConvertableDirectoryPath packagesDir, ConvertableFilePath artifactsFile, string feedApiKey, string codeFeedUrl, string symbolFeedUrl) { Information($"{nameof(PublishPackages)}: Publishing to NuGet..."); var artifacts = _File_.ReadAllLines(artifactsFile) .Distinct() .Where(a => a.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase)) .ToList(); var skippable = new List { "ReleaseNotes.md", // skip always // "Ocelot.24.0.0", // "Ocelot.Cache.CacheManager", // "Ocelot.Provider.Consul", // "Ocelot.Provider.Eureka", // "Ocelot.Provider.Kubernetes", // "Ocelot.Provider.Polly", // "Ocelot.Tracing.Butterfly", // "Ocelot.Tracing.OpenTracing", }; var includedInTheRelease = new List { "Ocelot.Provider.Kubernetes", }; foreach (var artifact in artifacts) { if (skippable.Exists(x => artifact.StartsWith(x, StringComparison.OrdinalIgnoreCase))) continue; // if (!includedInTheRelease.Exists(x => artifact.StartsWith(x))) continue; var package = packagesDir + File(artifact); Information($"{nameof(PublishPackages)}: Pushing package " + package + "..."); try { DotNetNuGetPush(package, new DotNetNuGetPushSettings { ApiKey = feedApiKey, Source = codeFeedUrl, SkipDuplicate = true }); var symbolArtifact = artifact.Replace(".nupkg", ".snupkg"); var symbolPackage = packagesDir + File(symbolArtifact); if (FileExists(symbolPackage)) { Information($" Pushing symbol package {symbolPackage}..."); System.Threading.Thread.Sleep(1000); try { DotNetNuGetPush(symbolPackage, new DotNetNuGetPushSettings { ApiKey = feedApiKey, Source = codeFeedUrl, SkipDuplicate = true }); } catch (Exception symEx) { Warning($" Symbol push failed: {symEx.Message}"); } } else { Information($" No symbol package found for {artifact}"); } } catch (Exception ex) { Information("--------------------------------------------------------------"); Warning(ex.ToString()); throw; // exit task with non-zero result -> failed step -> failed job in Actions } // catch (Exception ex) // { // Warning(ex.ToString()); // // bool isConflict = ex.ToString().Contains("409") || ex.ToString().Contains("Conflict"); // if (!isBeta /*|| !isConflict*/) throw; // var match = Regex.Match(theArtifact, @"-beta\.(\d+)(?=\.nupkg$)"); // if (!match.Success) // { // Warning(" No beta version found in the artifact name, but it should be there. Artifact: " + theArtifact); // break; // } // var betaNumber = match.Groups[1].Value; // Information($" Detected Beta number: {betaNumber}"); // int newBetaVer = int.Parse(betaNumber) + 1; // increase beta version by 1 trying to find the next free beta number // var newArtifact = Regex.Replace(theArtifact, @"-beta\.\d+(?=\.nupkg$)", "-beta." + newBetaVer); // var newPackage = packagesDir + File(newArtifact); // if (FileExists(newPackage)) DeleteFile(newPackage); // MoveFile(package, newPackage); // Warning($" Package renamed: {package} -> {newPackage} (Attempt #{attempts})"); // var oldSymbol = packagesDir + File(theArtifact.Replace(".nupkg", ".snupkg")); // var newSymbol = packagesDir + File(newArtifact.Replace(".nupkg", ".snupkg")); // if (FileExists(oldSymbol)) // { // if (FileExists(newSymbol)) DeleteFile(newSymbol); // MoveFile(oldSymbol, newSymbol); // } // package = newPackage; // theArtifact = newArtifact; // System.Threading.Thread.Sleep(1000); // } } } private bool PackageExists(string packageId, string version) { var url = $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/{version}/{packageId.ToLowerInvariant()}.{version}.nupkg"; try { var response = HttpGet(url); Information($"{nameof(PackageExists)}: Package ver.{packageId}.{version} exists"); return true; } catch (Exception ex) { Warning(ex.ToString()); Information($"{nameof(PackageExists)}: Package ver.{packageId}.{version} does NOT exist"); } return false; } private void SetupGitHubClient(System.Net.Http.HttpClient client) { string token = Environment.GetEnvironmentVariable("OCELOT_GITHUB_API_KEY"); client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); client.DefaultRequestHeaders.Add("User-Agent", "Ocelot Release"); client.DefaultRequestHeaders.Add("Accept", "application/vnd.github+json"); client.DefaultRequestHeaders.Add("X-GitHub-Api-Version", "2022-11-28"); } private dynamic CreateGitHubRelease() { var body = ReleaseNotesAsJson(); var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"{versioning.BranchName}\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"{body}\", \"draft\": true, \"prerelease\": true, \"generate_release_notes\": false }}"; var content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); using (var client = new System.Net.Http.HttpClient()) { SetupGitHubClient(client); var result = client.PostAsync("https://api.github.com/repos/ThreeMammals/Ocelot/releases", content).Result; if (result.StatusCode != System.Net.HttpStatusCode.Created) { var msg = "CreateGitHubRelease: StatusCode = " + result.StatusCode; Information(msg); throw new Exception(msg); } var releaseData = result.Content.ReadAsStringAsync().Result; dynamic releaseJSON = Newtonsoft.Json.JsonConvert.DeserializeObject(releaseData); Information("CreateGitHubRelease: Release ID is " + releaseJSON.id); return releaseJSON; } } private string ReleaseNotesAsJson() { var body = _File_.ReadAllText(releaseNotesFile, System.Text.Encoding.UTF8); return System.Text.Encodings.Web.JavaScriptEncoder.Default.Encode(body); } private void UploadFileToGitHubRelease(dynamic release, FilePath file) { var data = _File_.ReadAllBytes(file.FullPath); var content = new System.Net.Http.ByteArrayContent(data); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream"); using (var client = new System.Net.Http.HttpClient()) { SetupGitHubClient(client); int releaseId = release.id; var fileName = file.GetFilename(); string uploadUrl = release.upload_url.ToString(); // Information($"UploadFileToGitHubRelease: uploadUrl is {uploadUrl}"); string[] parts = uploadUrl.Replace("{", "").Split(','); uploadUrl = parts[0] + "=" + fileName; // $"https://uploads.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}/assets?name={fileName}" Information($"UploadFileToGitHubRelease: uploadUrl is {uploadUrl}"); var result = client.PostAsync(uploadUrl, content).Result; if (result.StatusCode != System.Net.HttpStatusCode.Created) { Information($"UploadFileToGitHubRelease: StatusCode is {result.StatusCode}. Release ID is {releaseId}. Failed to upload file '{fileName}' to URL: {uploadUrl}"); throw new Exception("UploadFileToGitHubRelease: StatusCode is " + result.StatusCode); } } } private void CompleteGitHubRelease(dynamic release) { int releaseId = release.id; string url = release.url.ToString(); string body = ReleaseNotesAsJson(); bool isPreRelease = !IsMainBranch(); var json = $"{{ \"tag_name\": \"{versioning.NuGetVersion}\", \"target_commitish\": \"{versioning.BranchName}\", \"name\": \"{versioning.NuGetVersion}\", \"body\": \"{body}\", \"draft\": false, \"prerelease\": {isPreRelease.ToString().ToLower()} }}"; var request = new System.Net.Http.HttpRequestMessage(new System.Net.Http.HttpMethod("Patch"), url); // $"https://api.github.com/repos/ThreeMammals/Ocelot/releases/{releaseId}"); request.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); using (var client = new System.Net.Http.HttpClient()) { SetupGitHubClient(client); var result = client.SendAsync(request).Result; if (result.StatusCode != System.Net.HttpStatusCode.OK) { Information($"CompleteGitHubRelease: StatusCode is {result.StatusCode}. Release ID is {releaseId}. Failed to patch release with URL: {url}"); throw new Exception("CompleteGitHubRelease: StatusCode = " + result.StatusCode); } } } /// gets the resource from the specified url private async Task GetResourceAsync(string url) { try { Information("Getting resource from " + url); using var client = new System.Net.Http.HttpClient(); client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github.v3+json"); client.DefaultRequestHeaders.UserAgent.ParseAdd("BuildScript"); using var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); Information("Response is >>>" + NL + content + NL + "<<<"); return content; } catch(Exception exception) { Information("There was an exception " + exception); throw; } } private bool IsRunningInCICD() => IsRunningOnCircleCI() || IsRunningInGitHubActions(); private bool IsRunningOnCircleCI() => !string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("CIRCLECI")); private bool IsRunningInGitHubActions() => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true"; private bool IsMainBranch() { var br = GetBranchName().ToLower(); return br == "main"; } private string GetBranchName() { return versioning?.BranchName ?? GetGitBranch(); } private string GetGitBranch() { var lines = GitHelper("branch --show-current"); var branch = string.Join(string.Empty, lines); return branch ?? "Unknown Branch"; } ================================================ FILE: codeanalysis.ruleset ================================================  ================================================ FILE: codecov.yml ================================================ coverage: status: project: #add everything under here, more options at https://docs.codecov.com/docs/commit-status default: target: auto threshold: 0% base: auto comment: layout: "reach, diff, flags, tree" behavior: default github_checks: annotations: true ================================================ FILE: coverlet.runsettings ================================================ cobertura [Ocelot.Testing*]* false MissingAll ================================================ FILE: docker/Dockerfile.base ================================================ FROM mcr.microsoft.com/dotnet/sdk:9.0-alpine RUN apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib git openssh-client # Install .NET 8 SDK RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh RUN chmod u+x ./dotnet-install.sh RUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet # Generate and export the development SSL certificate RUN mkdir -p /certs RUN dotnet dev-certs https -ep /certs/cert.pem -p '' RUN chmod 644 /certs/cert.pem ENV ASPNETCORE_URLS="https://+;http://+" ENV ASPNETCORE_HTTPS_PORT=443 ENV ASPNETCORE_Kestrel__Certificates__Default__Password="" ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pem ================================================ FILE: docker/Dockerfile.build ================================================ # call from ocelot repo root with # docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . # docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . FROM ocelot2/circleci-build:latest ARG OCELOT_COVERALLS_TOKEN ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN WORKDIR /build COPY ./. . RUN dotnet tool restore RUN dotnet cake ================================================ FILE: docker/Dockerfile.release ================================================ # call from ocelot repo root with # docker build --platform linux/arm64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . # docker build --platform linux/amd64 --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN --build-arg OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY --build-arg OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN -f ./docker/Dockerfile.build . FROM ocelot2/circleci-build:latest ARG OCELOT_COVERALLS_TOKEN ARG OCELOT_NUTGET_API_KEY ARG OCELOT_GITHUB_API_KEY ENV OCELOT_COVERALLS_TOKEN=$OCELOT_COVERALLS_TOKEN ENV OCELOT_NUTGET_API_KEY=$OCELOT_NUTGET_API_KEY ENV OCELOT_GITHUB_API_KEY=$OCELOT_GITHUB_API_KEY WORKDIR /build COPY ./. . RUN dotnet tool restore RUN dotnet cake ================================================ FILE: docker/Dockerfile.windows ================================================ FROM mcr.microsoft.com/dotnet/sdk:9.0-nanoserver-ltsc2022 # Install PowerShell globally AND RUN powershell -Command "gci" #RUN dotnet tool install -g powershell && pwsh -Command "gci" RUN pwsh -Command "gci" # # Install .NET 8 SDK USER ContainerAdministrator RUN pwsh -Command "Invoke-WebRequest -OutFile dotnet-install.ps1 https://dot.net/v1/dotnet-install.ps1; gci" ENV DOTNET_GENERATE_ASPNET_CERTIFICATE=true RUN pwsh -Command "./dotnet-install.ps1 -Channel 8.0 -InstallDir 'C:\Program Files\dotnet\'; gci -Path C:\\ -Recurse -Include *8.0* -Directory" #RUN git status RUN dotnet --info # Generate and export the development SSL certificate #RUN mkdir -p certs #RUN dotnet dev-certs https -ep certs/cert.pem -p '' #RUN pwsh -Command "dotnet dev-certs https --clean; dotnet dev-certs https; exit 0;" #RUN pwsh -Command "dotnet dev-certs https --trust; exit 0;" RUN pwsh -Command "dotnet dev-certs https --check --trust; exit 0;" #ENV ASPNETCORE_URLS="https://+;http://+" ASPNETCORE_HTTPS_PORT=443 #ENV ASPNETCORE_Kestrel__Certificates__Default__Password="" ASPNETCORE_Kestrel__Certificates__Default__Path=certs/cert.pem ================================================ FILE: docker/README.md ================================================ # docker build This folder contains the `Dockerfile.*` and `build-*.sh` scripts to create the Ocelot build image & container. ## Account - Docker Hub | [Ocelot Gateway](https://hub.docker.com/u/ocelot2) ## Repositories - [circleci-build](https://hub.docker.com/r/ocelot2/circleci-build) ## Outdated Tags - [8.21.0](https://hub.docker.com/layers/ocelot2/circleci-build/8.21.0/images/sha256-edb46d37ab52d39a5b27dc63895e5944d4d491d1788744ed144ecb4303b94532?context=explore), uploaded on Nov 20, 2023. It contains .NET 8, 7 and 6 SDKs. It supports builds for `net6.0`, `net7.0` and `net8.0` frameworks. - [8.23.2](https://hub.docker.com/layers/ocelot2/circleci-build/8.23.2/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore), uploaded on Apr 3, 2024. It supports development SSL certificates by the command `dotnet dev-certs https`. - [latest](https://hub.docker.com/layers/ocelot2/circleci-build/latest/images/sha256-981d6f9e6e5ba54f6e044bca6fcf8b5197a8f3e6ce2b3cdfa9e6704ecd2ca969?context=explore) is version 8.23.2, uploaded on Apr 3, 2024. ## .NET 8-9 Tags ### Single SDK Tags - [sdk-8-alpine-lin.net8](https://hub.docker.com/layers/ocelot2/circleci-build/sdk-8-alpine-lin.net8/images/sha256-17c21438771641ba2a3320a6c13fe756a851d707a925188d9616485bfc757b22?context=explore), uploaded on Dec 3, 2024. This Linux OS image contains .NET 8 SDK only. It supports builds for `net8.0` framework. - [sdk-9-alpine-lin.net9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk-9-alpine-lin.net9/images/sha256-c20d82a52c1a7eebf86bcd32751960ae189e6c50575959ee0499b1d88541c9ea?context=explore), uploaded on Dec 3, 2024. This Linux OS image contains .NET 9 SDK only. It supports builds for `net9.0` framework. - [sdk8-nanoserver2022-win.net8](https://hub.docker.com/layers/ocelot2/circleci-build/sdk8-nanoserver2022-win.net8/images/sha256-20f21f4361d18301bc885e1f01ebefcd6b024802130db1296173cdfaac5d75e6?context=explore), uploaded on Dec 3, 2024. This Windows OS image contains .NET 8 SDK only. It supports builds for `net8.0` framework. - [sdk9-nano2022-win.net9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-nano2022-win.net9/images/sha256-37b718885f8cfb3480299f48044836f01aec2370e331667dd1811a4c94a4ce45?context=explore), uploaded on Dec 3, 2024. This Windows OS image contains .NET 9 SDK only. It supports builds for `net9.0` framework. ### Double SDKs Tags - [sdk9-alpine-lin.net8-9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-alpine-lin.net8-9/images/sha256-707924d144248178caff80578649f7d0e9b7c70ed51b6fb5171170c9a30a4eae?context=explore) aka [9.24.0](https://hub.docker.com/layers/ocelot2/circleci-build/9.24.0/images/sha256-707924d144248178caff80578649f7d0e9b7c70ed51b6fb5171170c9a30a4eae?context=explore), uploaded on Dec 3, 2023. This Linux OS image contains .NET 8 and 9 SDKs. It supports builds for `net8.0` and `net9.0` frameworks. - [sdk9-nano2022-win.net8-9](https://hub.docker.com/layers/ocelot2/circleci-build/sdk9-nano2022-win.net8-9/images/sha256-6e44a8fc52ab091ea43090b6ab182f7a05d7ac31402ce742a7e035323e584cf7?context=explore) aka [9.24.win](https://hub.docker.com/layers/ocelot2/circleci-build/9.24.win/images/sha256-6e44a8fc52ab091ea43090b6ab182f7a05d7ac31402ce742a7e035323e584cf7?context=explore), uploaded on Dec 4, 2023. This Windows OS image contains .NET 8 and 9 SDKs. It supports builds for `net8.0` and `net9.0` frameworks. ### Links - Docker Hub | [Microsoft dotnet Images](https://hub.docker.com/r/microsoft/dotnet) - GitHub | dotnet-docker | [.NET SDK](https://github.com/dotnet/dotnet-docker/blob/main/README.sdk.md) - StackOverflow | [PowerShell and process exit codes](https://stackoverflow.com/questions/57468522/powershell-and-process-exit-codes) ================================================ FILE: docker/build-windows.sh ================================================ version=9.24.win tag=sdk9-nano2022-win.net8-9 docker build --no-cache --platform windows/amd64 -t ocelot2/circleci-build -f Dockerfile.windows . docker tag ocelot2/circleci-build ocelot2/circleci-build:$tag docker push ocelot2/circleci-build:$tag docker tag ocelot2/circleci-build ocelot2/circleci-build:$version docker push ocelot2/circleci-build:$version ================================================ FILE: docker/build.sh ================================================ # This script builds the Ocelot Docker file # echo $DOCKER_PASS | docker login -u $DOCKER_USER --password-stdin # {DotNetSdkVer}.{OcelotVer} -> {.NET9}.{24.0} -> 9.24.0 #version=9.24.0 tag=sdk9-alpine-lin.net8-9 docker build --platform linux/amd64 -t ocelot2/circleci-build -f Dockerfile.base . docker tag ocelot2/circleci-build ocelot2/circleci-build:$tag docker push ocelot2/circleci-build:$tag # docker tag ocelot2/circleci-build ocelot2/circleci-build:$version # docker push ocelot2/circleci-build:$version ================================================ FILE: docker/outdated/Dockerfile.8.21.0.base ================================================ FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine RUN apk add bash icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib git openssh-client RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh RUN chmod u+x ./dotnet-install.sh # Install .NET 8 SDK (already included in the base image, but listed for consistency) RUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet # Install .NET 7 SDK RUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet # Install .NET 6 SDK RUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet ================================================ FILE: docker/outdated/Dockerfile.8.23.2.base ================================================ FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine RUN apk add bash icu-libs krb5-libs libgcc libintl libssl3 libstdc++ zlib git openssh-client RUN curl -L --output ./dotnet-install.sh https://dot.net/v1/dotnet-install.sh RUN chmod u+x ./dotnet-install.sh # Install .NET 8 SDK (already included in the base image, but listed for consistency) RUN ./dotnet-install.sh -c 8.0 -i /usr/share/dotnet # Install .NET 7 SDK RUN ./dotnet-install.sh -c 7.0 -i /usr/share/dotnet # Install .NET 6 SDK RUN ./dotnet-install.sh -c 6.0 -i /usr/share/dotnet # Generate and export the development certificate RUN dotnet dev-certs https -ep /certs/cert.pem -p '' && \ chmod 644 /certs/cert.pem ENV ASPNETCORE_URLS="https://+;http://+" ENV ASPNETCORE_HTTPS_PORT=443 ENV ASPNETCORE_Kestrel__Certificates__Default__Password="" ENV ASPNETCORE_Kestrel__Certificates__Default__Path=/certs/cert.pem ================================================ FILE: docs/Makefile ================================================ # Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ================================================ FILE: docs/_static/overrides.css ================================================ blockquote { font-size: 0.9em; } aside.footnote-list { font-size: 0.9em; } .img-valign-middle { vertical-align: middle; } .img-valign-bottom { vertical-align: bottom; } .img-valign-textbottom { vertical-align: text-bottom; } ================================================ FILE: docs/building/building.rst ================================================ .. role:: htm(raw) :format: html .. role:: pdf(raw) :format: latex pdflatex .. _Ocelot: https://github.com/ThreeMammals/Ocelot .. _Cake: https://cakebuild.net .. _Bash: https://www.gnu.org/software/bash .. _build.cake: https://github.com/ThreeMammals/Ocelot/blob/main/build.cake .. _GitHub Actions: https://docs.github.com/en/actions .. _NuGet: https://www.nuget.org/profiles/ThreeMammals Building ======== This document summarises the build and release process for the `Ocelot`_ project. The build scripts are written using `Cake`_ (C# Make), with relevant build tasks defined in the '`build.cake`_' file located in the root of the `Ocelot`_ project. The scripts are designed to be run by developers locally in a `Bash`_ terminal (on any OS), in Command Prompt (CMD) or PowerShell consoles (on Windows OS), or by a CI/CD server (currently `GitHub Actions`_), with minimal logic defined in the build server itself. The final goal of the build process is to create ``Ocelot.*`` `NuGet`_ packages (.nupkg files) for redistribution via the `NuGet`_ repository or manually. The build process consists of several steps: (1) compilation, (2) testing, (3) creating and publishing `NuGet`_ packages, and (4) making an official GitHub release. The build process requires pre-installed .NET SDKs on the build machine (host) for all target framework monikers: TFMs are ``net8.0`` and ``net9.0`` currently. In general, the build process is the same across all environments and tools, with a few differences described below. .. _b-in-ide: In IDE ------ .. _Release configuration: https://learn.microsoft.com/en-us/visualstudio/debugger/how-to-set-debug-and-release-configurations?view=vs-2022 In an IDE, a DevOps engineer can build the project in Visual Studio IDE or another IDE in `Release configuration`_ mode, but the latest .NET 8/9 SDKs must be pre-installed on the local machine. However, this approach is not practical because the generated '.nupkg' files must be uploaded to `NuGet`_ manually, and the GitHub release must also be created manually. A better approach is to utilize the '`build.cake`_' script :ref:`b-in-terminal`, which covers all building scenarios. .. _b-in-terminal: In terminal ----------- .. _./: https://github.com/ThreeMammals/Ocelot/tree/main/ Folder: `./`_ These are local machine or remote server building scenarios using build scripts, aka '`build.cake`_'. In these scenarios, the following two commands should be run in a terminal from the project's root folder: .. code-block:: shell dotnet tool restore && dotnet cake # In Bash terminal dotnet tool restore; dotnet cake # In PowerShell terminal .. _break: http://break.do **Note**: The default target task ("Default") is "Build", and output files will be stored in the ``./artifacts`` directory. To run a desired target task, you need to specify its *name*: .. code-block:: shell dotnet tool restore && dotnet cake --target=name # In Bash terminal dotnet tool restore; dotnet cake --target=name # In PowerShell terminal For example, - .. code-block:: shell dotnet cake --target=Build It runs a local build, performing compilation and testing only. - .. code-block:: shell dotnet cake --target=Version It checks the next version to be tagged in the Git repository during the next release, without performing compilation or testing tasks. - .. code-block:: shell dotnet cake --target=CreateReleaseNotes It generates Release Notes artifacts in the ``/artifacts/Packages`` folder using the ``ReleaseNotes.md`` template file. - .. code-block:: shell dotnet cake --target=Release It creates a release, consisting of the following steps: compilation, testing, generating release notes, creating .nupkg files, publishing `NuGet`_ packages, and finally, making a GitHub release. .. _dotnet-tools.json: https://github.com/ThreeMammals/Ocelot/blob/main/.config/dotnet-tools.json **Note 1**: The building tools for the ``dotnet tool restore`` command are configured in the `dotnet-tools.json`_ file. **Note 2**: Some targets (build tasks) require appropriate environment variables to be defined directly in the terminal session (aka secret tokens). .. _b-with-docker: With Docker ----------- .. _docker: https://github.com/ThreeMammals/Ocelot/tree/main/docker .. _Dockerfile.build: https://github.com/ThreeMammals/Ocelot/blob/main/docker/Dockerfile.build .. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 Folder: ./`docker`_ The best way to replicate the CI/CD process and build `Ocelot`_ locally is by using the `Dockerfile.build`_ file, which can be found in the '`docker`_' folder in the `Ocelot`_ root directory. For example, use the following command: .. code-block:: shell docker build --platform linux/amd64 -f ./docker/Dockerfile.build . You may need to adjust the platform flag depending on your system. **Note**: This approach is somewhat excessive, but it will work if you are a masterful Docker user. 🙂 The Ocelot team has not followed this approach since version `24.0`_, favoring :ref:`b-with-ci-cd`-based builds and occasionally building :ref:`b-in-terminal` instead. .. _b-with-ci-cd: With CI/CD ---------- .. _workflows: https://github.com/ThreeMammals/Ocelot/tree/main/.github/workflows .. _PR: https://github.com/ThreeMammals/Ocelot/actions/workflows/pr.yml .. _Develop: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml .. _Release: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml .. _Coveralls: https://coveralls.io .. |ReleaseButton| image:: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml/badge.svg :target: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml :alt: Release Status :class: img-valign-textbottom .. |DevelopButton| image:: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml/badge.svg :target: https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml :alt: Development Status :class: img-valign-textbottom .. |DevelopCoveralls| image:: https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=develop :target: https://coveralls.io/github/ThreeMammals/Ocelot?branch=develop :alt: Coveralls Status :class: img-valign-textbottom .. |ReleaseCoveralls| image:: https://coveralls.io/repos/github/ThreeMammals/Ocelot/badge.svg?branch=main :target: https://coveralls.io/github/ThreeMammals/Ocelot?branch=main :alt: Coveralls Status :class: img-valign-textbottom .. _break2: http://break.do | Folder: ./.github/`workflows`_ | Provider: `GitHub Actions`_ | Workflows: `PR`_, `Develop`_, `Release`_ | Dashboard: `Workflow runs `_ (Actions tab) The `Ocelot`_ project utilizes `GitHub Actions`_ as a CI/CD provider, offering seamless integrations with the GitHub ecosystem and APIs. Starting from version `24.0`_, all pull requests, development commits, and releases are built using `GitHub Actions`_ workflows. There are three `workflows`_: one for pull requests (`PR`_), one for the ``develop`` branch (`Develop`_), and one for the ``main`` branch (`Release`_). **Note**: Each workflow has a dedicated status badge in the `Ocelot README`_: the |ReleaseButton|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml}{Release}` button and the |DevelopButton|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/actions/workflows/develop.yml}{Develop}` button, with the `PR`_ status being published directly in a pull request under the "Checks" tab. The `PR`_ workflow will track code coverage using `Coveralls`_. After opening a pull request or submitting a new commit to a pull request, `Coveralls`_ will publish a short message with the current code coverage once the top commit is built. Considering that `Coveralls`_ retains the entire history but does not fail the build if coverage falls below the threshold, all workflows have a built-in 80% threshold, applied internally within the ``build-cake`` job, particularly during the "`Cake Build`_" step-action. If the code coverage of a newly opened pull request drops below the 80% threshold, the `'build-cake' job`_ will fail, logging an appropriate message in the "`Cake Build`_" step. **Note 1**: There are special code coverage badges in `Ocelot README`_: the `Develop`_ |DevelopCoveralls| button and the `Release`_ |ReleaseCoveralls| button. **Note 2**: The current code coverage of the `Ocelot`_ project is around 85-86%. The coverage threshold is subject to change in upcoming releases. All `Coveralls`_ builds can be viewed by navigating to the `ThreeMammals/Ocelot `_ project on Coveralls.io. Documentation ------------- .. _docs: https://github.com/ThreeMammals/Ocelot/tree/main/docs .. _.readthedocs.yaml: https://github.com/ThreeMammals/Ocelot/blob/main/.readthedocs.yaml .. _Read the Docs: https://about.readthedocs.com .. _Ocelot app: https://app.readthedocs.org/projects/ocelot/ .. _README: https://github.com/ThreeMammals/Ocelot/blob/main/docs/readme.md .. _Ocelot README: https://github.com/ThreeMammals/Ocelot/blob/main/README.md .. |ReleaseDocs| image:: https://readthedocs.org/projects/ocelot/badge/?version=latest&style=flat-square :target: https://app.readthedocs.org/projects/ocelot/builds/?version__slug=latest :alt: ReadTheDocs Status :class: img-valign-middle .. |DevelopDocs| image:: https://readthedocs.org/projects/ocelot/badge/?version=develop&style=flat-square :target: https://app.readthedocs.org/projects/ocelot/builds/?version__slug=develop :alt: ReadTheDocs Status :class: img-valign-middle .. _break3: http://break.do | Folder: ./`docs`_ | Dashboard: `Ocelot app`_ project Documentation building is configured using the '`.readthedocs.yaml`_' integration file, which allows builds to run separately via the `Read the Docs`_ publisher. All build artifacts and document sources are located in the '`docs`_' folder. More details on the documentation build process can be found in the `README`_. **Note 1**: Documentation builds have a dedicated status badges in `Ocelot README`_: the `Develop`_ |DevelopDocs| button and the `Release`_ |ReleaseDocs| button. **Note**: Documentation can be easily built locally in a terminal from the '`docs`_' folder by running the ``make.sh`` or ``make.bat`` scripts. The resulting documentation build files will be located in the ``./docs/_build`` folder, with the HTML documentation specifically written to the ``./docs/_build/html`` folder. .. _b-testing: Testing ------- The tests should run and function correctly as part of the *building* process using the ``dotnet test`` command. You can also run them in Visual Studio IDE within the Test Explorer window. Depending on your build scenario, `Ocelot`_ *testing* can be performed as follows. :ref:`b-in-ide`: Simply run tests via the Test Explorer window of Visual Studio IDE. :ref:`b-in-terminal`: There are two main approaches: 1. Run the ``dotnet test`` command to perform all tests (unit, integration, and acceptance): .. code-block:: shell dotnet test -f net8.0 ./Ocelot.sln Or run tests separately per project: .. code-block:: shell dotnet test -f net8.0 ./test/Ocelot.UnitTests/Ocelot.UnitTests.csproj # Unit tests only dotnet test -f net8.0 ./test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj # Integration tests only dotnet test -f net8.0 ./test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj # Acceptance tests only 2. Run ``dotnet cake`` command: ``dotnet cake --target=Tests`` to perform all tests (unit, integration and acceptance). Or run tests separately per *testing* project: .. code-block:: shell dotnet cake --target=UnitTests # unit tests only dotnet cake --target=IntegrationTests # integration tests only dotnet cake --target=AcceptanceTests # acceptance tests only :ref:`b-with-docker`: This approach is not recommended. Instead, perform automated testing :ref:`b-with-ci-cd` or opt for :ref:`b-in-terminal`-based testing, which is a more advanced method. :ref:`b-with-ci-cd`: In `GitHub Actions`_ `workflows`_, the *testing* process consists of separate testing steps, organized per job: * In the `'build' job`_: There are '`Unit Tests`_', '`Integration Tests`_', and '`Acceptance Tests`_' steps. * In the `'build-cake' job`_: There is a '`Cake Build`_' step responsible for performing tests internally. .. _'build' job: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+build%3A+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code .. _Unit Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Unit+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code .. _Integration Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Integration+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code .. _Acceptance Tests: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22Acceptance+Tests%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code .. _'build-cake' job: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22-cake%3A%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code .. _Cake Build: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22cake-build%2F%22+path%3A%2F%5E%5C.github%5C%2Fworkflows%5C%2F%2F&type=code SSL certificate --------------- To create a certificate for :ref:`b-testing`, you can use `OpenSSL `_: * Install the `openssl `__ package (if you are using Windows, download the binaries `here `_). * Generate a private key: .. code-block:: bash openssl genrsa 2048 > private.pem * Generate a self-signed certificate: .. code-block:: bash openssl req -x509 -days 1000 -new -key private.pem -out public.pem * If needed, create a PFX file: .. code-block:: bash openssl pkcs12 -export -in public.pem -inkey private.pem -out mycert.pfx ================================================ FILE: docs/building/devprocess.rst ================================================ .. _Gitflow Workflow: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow .. _GitHub Flow: https://docs.github.com/en/get-started/using-github/github-flow .. _develop: https://github.com/ThreeMammals/Ocelot/tree/develop .. _main: https://github.com/ThreeMammals/Ocelot/tree/main .. _issue(s): https://github.com/ThreeMammals/Ocelot/issues .. _discussion: https://github.com/ThreeMammals/Ocelot/discussions .. _fork: https://docs.github.com/en/get-started/quickstart/fork-a-repo .. _unit: https://github.com/ThreeMammals/Ocelot/tree/develop/test/Ocelot.UnitTests .. _acceptance: https://github.com/ThreeMammals/Ocelot/tree/develop/test/Ocelot.AcceptanceTests .. _documentation: https://github.com/ThreeMammals/Ocelot/tree/develop/docs .. _feature: https://github.com/ThreeMammals/Ocelot/tree/develop/docs/features .. _Actions: https://github.com/ThreeMammals/Ocelot/actions .. _Coveralls check: https://coveralls.io/github/ThreeMammals/Ocelot Development Process =================== * The *development process* is optimized when using Gitflow branching, as detailed here: `Gitflow Workflow`_. It's important to note that the Ocelot team does not utilize `GitHub Flow`_, which, despite being quicker, does not align with the efficiency required for Ocelot's delivery. * Contributors are free to manage their pull requests and feature branches as they see fit to contribute to the '`develop`_' branch. * Maintainers have the autonomy to handle pull requests and merges. Any merges to the '`main`_' branch will trigger the release of packages to GitHub and NuGet. * In conclusion, while users should adhere to the guidelines in :doc:`../building/devprocess`, maintainers should follow the procedures outlined in :doc:`../building/releaseprocess`. Stages ------ Ocelot project follows this *development process* to integrate work into a merged commit in the '`develop`_' branch: 1. Users either create a new issue or select an existing `issue(s)`_ on GitHub. Issues can also be generated from `discussion`_ topics when necessary and agreed upon. 2. Users should create a `fork`_ and branch off of it (unless they are a core team member, in which case they can branch directly from the main/head/upstream repository), e.g., ``feature/xxx``, ``bug/xxx``, etc. The "xxx" can be the issue number or a brief description. 3. Once contributors are satisfied with their work, they can submit a pull request against the `develop`_ branch on GitHub with their changes. 4. The Ocelot team will review the pull request and, if satisfactory, merge it; otherwise, they will provide feedback for the contributor to address. To expedite pull request approval, contributors should consider: - Ensuring all changes are covered by `unit`_ and `acceptance`_ tests. - Ensuring that the code coverage percentage from `unit`_ tests does not decrease; thus, the `Coveralls check`_ reports a green status. - Updating any `documentation`_ affected by the changes, with a required review of the appropriate `feature`_ document. - Verifying that the feature is necessary and does not duplicate existing Ocelot features. 5. A pull request must meet the following criteria before merging: - All new code must be covered by `unit`_ tests. - There must be at least one `acceptance`_ test for the happy path of the new code. - Tests must pass locally, in Visual Studio Test Explorer or in terminal after performing ``dotnet test`` command. - The build must have a green status on repository `Actions`_ as passed *checks* of the pull request (aka `Checks`_ tab). - The build's performance must not be significantly degraded on repository `Actions`_ page for `PR`_ workflow. - The main `Ocelot package`_ must not introduce any non-Microsoft `dependencies`_. .. _PR: https://github.com/ThreeMammals/Ocelot/actions/workflows/pr.yml .. _Checks: https://github.com/ThreeMammals/Ocelot/pull/2283/checks .. _Ocelot package: https://www.nuget.org/packages/Ocelot .. _dependencies: https://www.nuget.org/packages/Ocelot#dependencies-body-tab .. _NuGet packages: https://www.nuget.org/profiles/ThreeMammals 6. Once the pull request is merged with *"Squash and Merge"* option into the `develop`_ branch, the ``Ocelot.*`` `NuGet packages`_ will not be updated until a release is crafted. The concluding step involves returning to GitHub to close any resolved `issue(s)`_. Notes ----- **Note 1**: The `issue(s)`_ linked to the pull request within the *Development* settings (on the right sidebar of the pull request settings) will automatically close upon merging. It is crucial for developers to utilize the *"Link an issue from this repository"* feature in the *Development* settings. An alternative way to link `issue(s)`_ is by specifying them in the pull request description, where the developer lists the linked `issue(s)`_ that need to be closed. For example: .. code-block:: markdown ## Fixes #1222 - #1222 ## Closes #1333 - #1333 ## Proposed Changes - change 1 - change 2 This Markdown should automatically link the desired bug/issue in the open status. For bugs, the developer needs to write "Fixes #xxxx", and for features, "Closes #xxxx". .. _GitHub Actions: https://docs.github.com/en/actions .. _workflows: https://github.com/ThreeMammals/Ocelot/tree/main/.github/workflows **Note 2**: All pull request builds are conducted using `GitHub Actions`_, but developers have the freedom to build Ocelot as needed. Details can be found in the :doc:`../building/building` chapter. Additionally, for a deeper understanding of the current Ocelot CI/CD environment and a clearer view of the CI/CD build process, refer to the "Building :ref:`b-with-ci-cd`" section. **Note 3**: Should you encounter any confusion or obstacles, do not hesitate to reach out to the members of the 'Ocelot Team' or the repository maintainers. .. _dev-best-practices: Best Practices -------------- * Refer to the Ocelot `Actions`_ dashboard on GitHub to verify the latest build statuses for the three current `workflows`_. It is recommended to monitor the build status of each workflow on the `Actions`_ dashboard or directly in the `Checks`_ tab of a pull request. If a build fails, initiate a new build by pushing a new commit, or consult with online maintainers or code reviewers to ensure the current pull request build is successful. * Request a code review after reaching the "Development Complete" stage, and address all feedback issues. Code is deemed complete when robust code, relevant `unit`_ and `acceptance`_ tests, and `documentation`_ updates are in place. * Set up your development environment on Windows OS using Visual Studio IDE. While development in Linux OS with alternative IDEs is possible, it is not recommended. For more details, refer to the :ref:`dev-fun` section. * Remain online after submitting a pull request/issue to ensure maintainers can reach you promptly. Note that if you are offline for extended periods, such as days, weeks, or months, maintainers may deprioritize your work. A strong contribution ethic implies constant online presence and proactivity. .. _dev-fun: Dev Fun ------- This section is part of the :ref:`dev-best-practices` and is written to be more amusing D) EOL Gotchas ^^^^^^^^^^^ *Also known as, "Line-Endings problem"* Since the project's inception in 2016, this issue has been persistent. Indeed, some lines end with the ``LF`` character, typical of the Linux OS. Many of our contributors work on Linux and use IDEs like Visual Studio Code, JetBrains .NET Rider, which defaults to the ``LF`` as the newline character. As a result, we have numerous files with inconsistent or mixed EOL characters. This problem stems from the well-known dilemma of End-of-Line (EOL) characters in cross-OS development. For the Windows OS, the EOL character is ``CRLF``, while for Linux, it is ``LF``. Modern IDEs and Git repositories have their own strategies for detecting inconsistencies of mixed EOLs in source files. However, the GitHub "Files Changed" tool unfortunately registers a line change in two scenarios: ``CRLF`` to ``LF`` and ``LF`` to ``CRLF``, even when there's no actual code change! Reviewing such pull requests with fictitious ("fake") changes is always challenging because the reviewer's focus should be on actual code changes. Please note, if a pull request is filled with "fake" changes in *"Files Changed"*, the code reviewer has the right to not provide a code review, mark the PR as a draft, or even close it. Our standard practice is to maintain end-of-line characters as they are. Moreover, we utilize Visual Studio's unique ``.editorconfig`` IDE analyzer settings for EOL to avoid issues with line endings. These settings are specific to Visual Studio, hence we recommend rebasing a feature branch onto develop using Visual Studio exclusively. Special EOL settings can be specified in the ``.gitattributes`` file of the git repository, although we do not currently manage this. Our current recommendations for addressing the end-of-line (EOL) issue are as follows: * Ideally, resolve merge conflicts by prioritizing the changes in the `develop`_ branch, then manually incorporate your changes in the merge tool dialog. It appears that changes from the feature branch are being included, even if they are minor. Conflicts should be addressed by manually applying your changes to the `develop`_ branch with a merge tool. * If changes from the feature branch are given priority (despite being minor), the merge tool will document them and apply ``CRLF`` end-of-line characters according to the rules specified in ``.editorconfig``. This is the source of the issue. * Renaming a method in an IDE, such as Visual Studio, or using another auto-refactoring command, causes Visual Studio to apply the command using the default styling rules in ``.editorconfig``, which includes `CRLF settings `_. Thus, applying auto-refactoring commands inadvertently alters the EOL characters, leading to "fake" changes in pull requests. Note that Visual Studio analyzers (IDE, StyleCop, etc.) may also recommend auto-refactoring, which could be applied implicitly. To preserve the original EOL characters, manual code editing is necessary. Therefore, "fake" changes result from auto-refactoring commands in IDEs like Visual Studio, Visual Code, Rider, etc. * **Our final recommendation** is to boot into Windows, use Visual Studio Community (which is free), refrain from using auto-refactoring commands, and ensure that EOLs remain unchanged. If your OS differs, you **must** ensure that the appropriate settings are provided in the ``.gitattributes`` file to always commit files with ``CRLF`` EOL characters. ================================================ FILE: docs/building/releaseprocess.rst ================================================ .. _Gitflow Workflow: https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow .. _GitHub Flow: https://docs.github.com/en/get-started/using-github/github-flow .. _develop: https://github.com/ThreeMammals/Ocelot/tree/develop .. _main: https://github.com/ThreeMammals/Ocelot/tree/main Release Process =============== * The *release process* is optimized when using Gitflow branching, as detailed here: `Gitflow Workflow`_. It's important to note that the Ocelot team does not utilize `GitHub Flow`_, which, despite being quicker, does not align with the efficiency required for Ocelot's delivery. * Contributors are free to manage their pull requests and feature branches as they see fit to contribute to the '`develop`_' branch. * Maintainers have the autonomy to handle pull requests and merges. Any merges to the '`main`_' branch will trigger the release of packages to GitHub and NuGet. * In conclusion, while users should adhere to the guidelines in :doc:`../building/devprocess`, maintainers should follow the procedures outlined in :doc:`../building/releaseprocess`. Stages ------ .. _Pair Programming: https://www.bing.com/search?q=Pair+Programming .. _SemVer: https://semver.org .. _GitVersion: https://gitversion.net/docs/ .. _Ocelot NuGet packages: https://www.nuget.org/profiles/ThreeMammals .. _Release: https://github.com/ThreeMammals/Ocelot/actions/workflows/release.yml .. _Environments: https://github.com/ThreeMammals/Ocelot/settings/environments .. _build.cake: https://github.com/ThreeMammals/Ocelot/blob/main/build.cake .. _ThreeMammals: https://github.com/ThreeMammals .. _milestone: https://github.com/ThreeMammals/Ocelot/milestones .. _releases: https://github.com/ThreeMammals/Ocelot/releases Ocelot project follows this *release process* to incorporate work into NuGet packages: 1. As a code reviewers, maintainers review pull requests and, if satisfactory, merge them; otherwise, they provide feedback for the contributor to address. Contributors are supported through continuous `Pair Programming`_ sessions, which include multiple code reviews, resolving code review issues, and problem-solving. 2. As a release engineers, maintainers must adhere to Semantic Versioning (`SemVer`_) supported by `GitVersion`_. For breaking changes, maintainers should use the correct commit message (containing *"+semver: breaking|major|minor|patch"*) to ensure `GitVersion`_ applies the appropriate `SemVer`_ tags. Manual tagging of the Ocelot repository should be avoided to prevent disruptions. 3. Once a pull request is merged into the '`develop`_' branch, the `Ocelot NuGet packages`_ remain unchanged until a release is initiated. When sufficient work warrants a new release, the '`develop`_' branch is merged into '`main`_' as a ``release/X.Y`` branch, triggering the `Release`_ workflow that builds the code, assigns versions, and pushes artifacts to GitHub and packages to NuGet. 4. The release engineer, who holds the integration tokens in GitHub `Environments`_, automates each release build using the primary build script, '`build.cake`_'. Automated or manual :doc:`../building/building` can be performed :ref:`b-in-terminal` or :ref:`b-with-ci-cd`. The release engineer is also responsible for DevOps within the `ThreeMammals`_ organization, across all (sub)repositories, supporting the primary build script, and scripting for other repositories. 5. The release engineer drafts the ``ReleaseNotes.md`` template file, informing the community about key aspects of the release, including new or updated features, bug fixes, documentation updates, breaking changes, contributor acknowledgments, version upgrade guidelines, and more. 6. The final stage of the *release process* involves returning to GitHub to close the current `milestone`_, ensuring that: * All issues within the `milestone`_ are closed; any remaining work from open issues should be transferred to the next `milestone`_. * All pull requests associated with the `milestone`_ are either closed or reassigned to the upcoming release `milestone`_. * Release Notes are published on GitHub `releases`_, with an additional review of the text. * The published release is designated as the latest, provided the corresponding `Ocelot NuGet packages`_ have been successfully uploaded to the `ThreeMammals `__ account. 7. Optional support for the major version ``X.Y.0`` should be available in cases such as Microsoft official patches and critical Ocelot defects of that major version. Maintainers should release patched versions ``X.Y.1-z`` as hot-fix patch versions. Notes ----- .. _GitHub Actions: https://docs.github.com/en/actions .. _Actions: https://github.com/ThreeMammals/Ocelot/actions .. _Tom Pallister: https://github.com/TomPallister .. _Raman Maksimchuk: https://github.com/raman-m .. _Ocelot Team: https://github.com/orgs/ThreeMammals/teams **Note 1**: All NuGet package builds and releases are conducted through the `GitHub Actions`_ CI/CD provider. For details, refer to the dedicated `Actions`_ dashboard, which should be used to monitor the current status of three workflows. **Note 2**: Currently, only `Tom Pallister`_, `Raman Maksimchuk`_, the owners—along with the `Ocelot Team`_ maintainers—have the authority to merge releases into the '`main`_' branch of the Ocelot repository. This policy ensures that final :ref:`quality-gates` are in place. The maintainers' primary focus during the final merge is to identify any security issues, as outlined in Stage 7 of the process. .. _quality-gates: Quality Gates ------------- .. _code analysis rule set: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20%3CCodeAnalysisRuleSet%3E&type=code .. _codeanalysis.ruleset: https://github.com/ThreeMammals/Ocelot/blob/main/codeanalysis.ruleset .. _Overview of .NET source code analysis: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview?tabs=net-9 .. _StyleCop.Analyzers: https://www.nuget.org/packages/StyleCop.Analyzers .. _reference: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20StyleCop.Analyzers&type=code .. _here: https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/configuration-options **Gate 1**: Static code analysis. The Ocelot repository includes the following integrated style analyzers: * In-built IDE (.NET SDK): The `code analysis rule set`_ is defined in the '`codeanalysis.ruleset`_' file, with configuration instructions available `here`_. For comprehensive documentation, refer to the following article: - Microsoft Learn: `Overview of .NET source code analysis`_ * `StyleCop.Analyzers`_: The package is somewhat outdated with slow support, but Ocelot projects still `reference`_ it because it has remained functional since 2015/16 as an older style analyzer. The Ocelot team plans to replace this library with a more advanced tool in upcoming releases. ================================================ FILE: docs/conf.py ================================================ # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'Ocelot Gateway' copyright = ' 2016-2026 Three Mammals' author = 'Tom Gardham-Pallister, Raman Maksimchuk' release = '25.0 ".NET 10"' # OK displayed version = '25.0' # version is not displayed in either HTML pages or PDF docs # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ 'sphinx_copybutton' ] templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output # HTML theming: https://www.sphinx-doc.org/en/master/usage/theming.html # HTML theme development: https://www.sphinx-doc.org/en/master/development/html_themes/index.html # https://alabaster.readthedocs.io/en/latest/ # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_theme html_theme = 'alabaster' # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_static_path html_static_path = ['_static'] html_css_files = ['overrides.css'] ================================================ FILE: docs/features/administration.rst ================================================ .. _IdentityServer: https://github.com/DuendeArchive/IdentityServer4 .. _IdentityServer4: https://www.nuget.org/packages/IdentityServer4 .. _Program: https://github.com/ThreeMammals/Ocelot.Administration.IdentityServer4/blob/main/sample/Program.cs .. _Ocelot.Administration.IdentityServer4: https://www.nuget.org/packages/Ocelot.Administration.IdentityServer4 .. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 .. _Ocelot.postman_collection.json: https://github.com/ThreeMammals/Ocelot.Administration.IdentityServer4/blob/main/sample/Ocelot.postman_collection.json Administration ============== **Ocelot extension package**: `Ocelot.Administration.IdentityServer4`_ with integrated `IdentityServer4`_ package by `IdentityServer org `_ (archived on March 6, 2025) Ocelot supports changing configuration during runtime via an authenticated HTTP API. This can be authenticated in two ways either using Ocelot's internal `IdentityServer`_ (for authenticating requests to the :ref:`administration-api` only) or hooking the :ref:`administration-api` authentication into your own `IdentityServer`_. The first thing you need to do if you want to use the :ref:`administration-api` is bring in the relevant `Ocelot.Administration.IdentityServer4`_ package: .. code-block:: powershell NuGet\Install-Package Ocelot.Administration.IdentityServer4 dotnet add package Ocelot.Administration.IdentityServer4 This will bring down everything needed by the :ref:`administration-api`. **Warning!** Currently, the *Administration* feature relies solely on the `IdentityServer4`_ package, whose `repository `_ was archived by its owner on July 31, 2024 (for the first time) and again on March 6, 2025. In release `24.0`_, the Ocelot team deprecated the `Ocelot.Administration.IdentityServer4`_ extension package. However, `the repository `_ remains available, allowing for potential patches. .. _ad-your-own-identityserver: Your Own IdentityServer [#f1]_ ------------------------------ All you need to do to hook into your own `IdentityServer`_ is add the following configuration options with authentication to your `Program`_. After that, we must pass these options to the ``AddAdministration()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f2]_, as shown below: .. code-block:: csharp Action options = o => { o.Authority = "https://identity-server-host:3333"; o.RequireHttpsMetadata = true; // false in development environment o.TokenValidationParameters = new() { ValidateAudience = false, }; //... }; builder.Services .AddOcelot(builder.Configuration) .AddAdministration("/administration", options); You now need to get a token from your `IdentityServer`_ and use in subsequent requests to Ocelot's :ref:`administration-api`. **Note**: This feature is useful because the `IdentityServer`_ authentication middleware needs the URL of the server. If you are using the :ref:`ad-internal-identityserver`, it might not always be possible to have the Ocelot URL. .. _ad-internal-identityserver: Internal IdentityServer ----------------------- The API is authenticated using Bearer tokens that you request from Ocelot itself. This is provided by the amazing `IdentityServer`_ project that the .NET community has been using for several years. Check it out. In order to enable the administration section, you need to do a few things. First of all, add this to your initial `Program`_. The path can be anything you want and it is obviously recommended don't use a URL you would like to route through with Ocelot as this will not work. The administration uses the ``MapWhen`` functionality of ASP.NET Core and all requests to ``{root}/administration`` will be sent there not to the Ocelot middleware. The secret is the client secret that Ocelot's internal `IdentityServer`_ will use to authenticate requests to the :ref:`administration-api`. This can be whatever you want it to be! In order to pass this secret string as parameter, we must call the ``AddAdministration()`` extension of the ``OcelotBuilder`` being returned by ``AddOcelot()`` [#f2]_, as shown below: .. code-block:: csharp builder.Services .AddOcelot(builder.Configuration) .AddAdministration("/administration", "secret"); In order for the :ref:`administration-api` to work, Ocelot and `IdentityServer`_ must be able to call themselves for validation. This means that you need to add the base URL of Ocelot to the global configuration if it is not the default ``http://localhost:5000``. **Note**: If you are using something like Docker to host Ocelot, it might not be able to call back to ``localhost``, etc., and you need to know what you are doing with Docker networking in this scenario. Configuration can be done as follows: * If you want to run on a different host and port locally: .. code-block:: json "GlobalConfiguration": { "BaseUrl": "http://localhost:5580" } * or if Ocelot is exposed via DNS: .. code-block:: json "GlobalConfiguration": { "BaseUrl": "http://mydns.net" } Now, if you went with the configuration options above and want to access the API, you can use the Postman scripts called `Ocelot.postman_collection.json`_ in the solution to change the Ocelot configuration. Obviously these will need to be changed if you are running Ocelot on a different URL to ``http://localhost:5000``. The scripts show you how to request a Bearer token from Ocelot and then use it to GET the existing configuration and POST a configuration. If you are running multiple Ocelot instances in a cluster then you need to use a certificate to sign the Bearer tokens used to access the :ref:`administration-api`. In order to do this, you need to add two more environmental variables for each Ocelot in the cluster: 1. ``OCELOT_CERTIFICATE``: The path to a certificate that can be used to sign the tokens. The certificate needs to be of the type X509 and obviously Ocelot needs to be able to access it. 2. ``OCELOT_CERTIFICATE_PASSWORD``: The password for the certificate. Normally Ocelot just uses temporary signing credentials but if you set these environmental variables then it will use the certificate. If all the other Ocelot instances in the cluster have the same certificate then you are good! .. _administration-api: Administration API ------------------ * **POST** ``{adminPath}/connect/token`` This gets a token for use with the admin area using the client credentials we talk about setting above. Under the hood this calls into an `IdentityServer`_ hosted within Ocelot. The body of the request is form-data as follows: * ``client_id`` set as admin * ``client_secret`` set as whatever you used when setting up the administration services. * ``scope`` set as admin * ``grant_type`` set as client_credentials * **GET** ``{adminPath}/configuration`` This gets the current Ocelot configuration. It is exactly the same JSON we use to set Ocelot up with in the first place. * **POST** ``{adminPath}/configuration`` This overwrites the existing configuration (should probably be a PUT!). We recommend getting your config from the GET endpoint, making any changes and posting it back... simples. The body of the request is JSON and it is the same format as the `FileConfiguration `_ that we use to set up Ocelot on a file system. Please note, if you want to use this API then the process running Ocelot must have permission to write to the disk where your ``ocelot.json`` or ``ocelot.{environment}.json`` is located. This is because Ocelot will overwrite them on save. * **DELETE** ``{adminPath}/outputcache/{region}`` This clears a region of the cache. If you are using a backplane, it will clear all instances of the cache! Giving your the ability to run a cluster of Ocelots and cache over all of them in memory and clear them all at the same time, so just use a distributed cache. The region is whatever you set against the ``Region`` field in the `FileCacheOptions `_ section of the Ocelot configuration. """" .. [#f1] The ":ref:`Your Own IdentityServer `" feature was implemented for issue `228 `_. .. [#f2] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. ================================================ FILE: docs/features/aggregation.rst ================================================ Aggregation =========== *Aggregation*, also known as HTTP response data aggregation, is a well-known Backend for Frontend pattern of Microservices architecture. * `Backend for Frontend (BFF) Pattern: Microservices for UX | Teleport Academy `_ * `Gateway Aggregation pattern | Azure Architecture Center | Microsoft Learn `_ * `Backends for Frontends pattern | Azure Architecture Center | Microsoft Learn `_ * `Implement API Gateways with Ocelot | .NET microservices - Architecture e-book | Microsoft Learn `_ Ocelot allows you to specify *Aggregate Routes* [#f1]_ that combine multiple normal routes and map their responses into a single object. This is particularly useful when a client is making multiple requests to a server that could be consolidated into one. This feature supports the implementation of a Backend for Frontend (BFF) architecture using Ocelot. Configuration ------------- .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json In order to set this up, you need to configure the `ocelot.json`_ file as follows. In this example, two normal routes are specified, each having a ``Key`` property. An *aggregation* is then defined, which combines the two routes using their keys listed in ``RouteKeys``, and the ``UpstreamPathTemplate`` is set up to function like a normal route. Note that duplicate ``UpstreamPathTemplates`` are not allowed between ``Routes`` and ``Aggregates``. You can use all of Ocelot's normal route options, except for ``RequestIdKey``, as explained in the :ref:`agg-gotchas` section. .. code-block:: json :emphasize-lines: 11, 21, 24 { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/laura", "DownstreamPathTemplate": "/", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 51881 } ], "Key": "Laura" }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/tom", "DownstreamPathTemplate": "/", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 51882 } ], "Key": "Tom" } ], "Aggregates": [ { "UpstreamPathTemplate": "/", "RouteKeys": [ "Tom", "Laura" ] } ] } You can also set ``UpstreamHost`` and ``RouteIsCaseSensitive`` in the *aggregation* configuration. These settings behave the same as in other routes. If the route ``/tom`` returned a body of ``{"Age": 19}`` and ``/laura`` returned ``{"Age": 25}``, the response after *aggregation* would be as follows: .. code-block:: json {"Tom":{"Age": 19},"Laura":{"Age": 25}} At the moment, the *aggregation* is quite simple. Ocelot retrieves the response from your downstream service and inserts it into a JSON dictionary, as shown above. The route ``Key`` becomes the key of the dictionary, and the response body from your downstream service serves as the value. The resulting object is plain JSON without any formatting or additional spaces. **Note 1**: All headers will be lost from the downstream service's response. **Note 2**: Ocelot will always return the content type ``application/json`` for an aggregate request. **Note 3**: If your downstream services return a ``404`` `Not Found `_, the aggregate will simply return nothing for that downstream service. It will not change the aggregate response to a ``404``, even if all the downstream services return a ``404``. .. _agg-complex-aggregation: Complex Aggregation [#f2]_ -------------------------- Imagine you would like to use aggregated queries but don't have all the parameters for your queries. First, you need to call an endpoint to obtain the necessary data, such as a user's ID, and then return the user's details. Let's say we have an endpoint that returns a series of comments referencing various users or threads. The author of the comments is identified by their ID, but you want to return all the details about the author. Here, you could use aggregation to: 1) retrieve all the comments, and 2) attach the author details. In fact, two endpoints are called, but for the second, you dynamically replace the user's ID in the route to obtain the details. In concrete terms: 1) ``/Comments`` contains the ``authorId`` property. 2) ``/users/{userId}``, with ``{userId}`` replaced by ``authorId``, is used to obtain the user's details. To perform the mapping, you need to use the ``RouteKeysConfig`` list of configuration options for aggreagte route, typed as ``AggregateRouteConfig`` class: .. code-block:: json "RouteKeysConfig": [ { "RouteKey": "UserDetails", "JsonPath": "$[*].authorId", "Parameter": "userId" } ] ``RouteKey`` is used as a reference for the route, ``JsonPath`` indicates where the parameter of interest is located in the first request's response body, and ``Parameter`` specifies that the value for ``authorId`` should be used as the request parameter ``userId``. The final configuration is as follows: .. code-block:: json :emphasize-lines: 27-30 { "Routes": [ { "UpstreamPathTemplate": "/Comments", "DownstreamPathTemplate": "/", // ... "Key": "Comments" }, { "UpstreamPathTemplate": "/UserDetails/{userId}", "DownstreamPathTemplate": "/users/{userId}", // ... "Key": "UserDetails" }, { "UpstreamPathTemplate": "/PostDetails/{postId}", "DownstreamPathTemplate": "/posts/{postId}", // ... "Key": "PostDetails" } ], "Aggregates": [ { "UpstreamPathTemplate": "/", "UpstreamHost": "localhost", "RouteKeys": [ "Comments", "UserDetails", "PostDetails" ], "RouteKeysConfig": [ { "RouteKey": "UserDetails", "JsonPath": "$[*].writerId", "Parameter": "userId" }, { "RouteKey": "PostDetails", "JsonPath": "$[*].postId", "Parameter": "postId" } ] } ] } Custom Aggregators ------------------ Ocelot started with basic request *aggregation*, and since then, a more advanced method has been added. This method allows the user to take the responses from downstream services and aggregate them into a response object. The `ocelot.json`_ setup is almost identical to the basic *aggregation* approach, except that you need to add an ``Aggregator`` property, as shown below: .. code-block:: json :emphasize-lines: 20 { "Routes": [ { "UpstreamPathTemplate": "/laura", "DownstreamPathTemplate": "/", // ... "Key": "Laura" }, { "UpstreamPathTemplate": "/tom", "DownstreamPathTemplate": "/", // ... "Key": "Tom" } ], "Aggregates": [ { "UpstreamPathTemplate": "/", "RouteKeys": [ "Tom", "Laura" ], "Aggregator": "MyAggregator" } ] } Here, we have added an aggregator called ``MyAggregator``. Ocelot will look for this aggregator when it tries to aggregate this route. In order to make the aggregator available in Ocelot Core, we must add the ``MyAggregator`` to the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f3]_, as shown below: .. code-block:: csharp :emphasize-lines: 5 using Ocelot.Multiplexer; builder.Services .AddOcelot(builder.Configuration) .AddSingletonDefinedAggregator(); Now, when Ocelot tries to aggregate the route above, it will find the ``MyAggregator`` in the DI-container and use it to aggregate the route. Since the ``MyAggregator`` is registered in the DI-container, you can add any dependencies it needs to the container, as shown below: .. code-block:: csharp :emphasize-lines: 2, 6 builder.Services .AddSingleton(); // ... builder.Services .AddOcelot(builder.Configuration) .AddSingletonDefinedAggregator(); In this example, ``MyAggregator`` depends on ``MyDependency``, and it will be resolved by the DI container. In addition to this, Ocelot lets you add transient aggregators, as shown below: .. code-block:: csharp :emphasize-lines: 3 builder.Services .AddOcelot(builder.Configuration) .AddTransientDefinedAggregator(); In order to create an *aggregator*, you must implement the following interface: .. code-block:: csharp public interface IDefinedAggregator { Task Aggregate(List responses); } With this feature, you can essentially do whatever you want, as the ``HttpContext`` objects contain the results of all the aggregate requests. Please note that if the ``HttpClient`` throws an exception when making a request to a route in the aggregate, you will not receive a ``HttpContext`` for it. However, you will receive one for any that succeed. If an exception is thrown, it will be logged. Below is an example of an *aggregator* that can be implemented for your solution: .. code-block:: csharp public class MyAggregator : IDefinedAggregator { public async Task Aggregate(List responseHttpContexts) { // The aggregator gets a list of downstream responses as parameter. // You can now implement your own logic to aggregate the responses (including bodies and headers) from the downstream services var responses = responseHttpContexts.Select(x => x.Items.DownstreamResponse()).ToArray(); // In this example we are concatenating the results, // but you could create a more complex construct, up to you. var contentList = new List(); foreach (var response in responses) { var content = await response.Content.ReadAsStringAsync(); contentList.Add(content); } // The only constraint here: You must return a DownstreamResponse object. return new DownstreamResponse( new StringContent(JsonConvert.SerializeObject(contentList)), HttpStatusCode.OK, responses.SelectMany(x => x.Headers).ToList(), "reason"); } } .. _agg-gotchas: Gotchas ------- * You cannot use routes with specific ``RequestIdKeys``, as this would be overly complicated to track. * *Aggregation* supports only the ``GET`` HTTP verb. * *Aggregation* allows the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data. Form data and attached files should also be forwarded. It is essential to specify the ``Content-Length`` header in requests to the upstream; otherwise, Ocelot will log warnings such as: *"Aggregation does not support body copy without a Content-Length header!"* """" .. [#f1] This feature was requested as part of issue `79`_, and further improvements were made as part of issue `298`_. A significant refactoring and revision of the `Multiplexer `_ design was carried out on March 4, 2024, in version `23.1`_. See pull requests `1462`_ and `1826`_ for more details. .. [#f2] The ":ref:`Complex Aggregation `" feature is still in its early stages, but it enables searching for data based on an initial request. This feature was requested as part of issue `661`_, introduced in pull request `704`_, and released in version `13.4`_. .. [#f3] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. .. _79: https://github.com/ThreeMammals/Ocelot/issues/79 .. _298: https://github.com/ThreeMammals/Ocelot/issues/298 .. _661: https://github.com/ThreeMammals/Ocelot/issues/661 .. _704: https://github.com/ThreeMammals/Ocelot/pull/704 .. _1462: https://github.com/ThreeMammals/Ocelot/pull/1462 .. _1826: https://github.com/ThreeMammals/Ocelot/pull/1826 .. _13.4: https://github.com/ThreeMammals/Ocelot/releases/tag/13.4.1 .. _23.1: https://github.com/ThreeMammals/Ocelot/releases/tag/23.1.0 ================================================ FILE: docs/features/authentication.rst ================================================ .. _scheme: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs Authentication ============== In order to authenticate routes and subsequently use any of Ocelot's claims based features such as authorization or modifying the request with values from the token, users must register authentication services in their `Program`_ as usual but they provide a `scheme`_ (authentication provider key) with each registration e.g. .. code-block:: csharp const string AuthenticationProviderKey = "MyKey"; // aka scheme builder.Services .AddAuthentication() .AddJwtBearer(AuthenticationProviderKey, options => { // authentication setup via options initialization }); In this example, ``MyKey`` is the `scheme`_ with which this provider has been registered, but for JWT bearer authentication, the scheme is usually ``Bearer``. We then map this to a route in the configuration using the following :ref:`authentication-options-schema` options: * ``AuthenticationProviderKey`` is a string, the legacy definition of :ref:`Single Authentication Scheme `. * ``AuthenticationProviderKeys`` is an array of strings, the recommended definition of :ref:`Multiple Authentication Schemes ` feature. .. _authentication-options-schema: ``AuthenticationOptions`` Schema -------------------------------- .. _FileAuthenticationOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs Class: `FileAuthenticationOptions`_ The following is the full *authentication* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`. Not all of these options need to be configured; however, the ``AuthenticationProviderKeys`` option is mandatory when ``AuthenticationProviderKey`` is absent. .. code-block:: json "AuthenticationOptions": { "AllowAnonymous": false, // nullable boolean "AllowedScopes": [], // array of strings "AuthenticationProviderKey": "", // deprecated! -> use AuthenticationProviderKeys "AuthenticationProviderKeys": [] // array of strings } .. list-table:: :widths: 25 75 :header-rows: 1 * - *Option* - *Description* * - ``AllowAnonymous`` - Excludes a route from global *authentication options* by setting it to ``true``. If the global option disables authentication by forcibly having a ``true`` value, then at the route level the option can include a route to be authenticated by setting it to ``false``. For more details, refer to the ":ref:`Configuration and AllowAnonymous `" section. * - ``AllowedScopes`` - If specified, enables authorization based on the ``scope`` claim after successful authentication by a configured authentication provider. For more details, refer to the ":ref:`authentication-allowed-scopes`" section. * - ``AuthenticationProviderKey`` - Maps a configured authentication provider, identified by a key (scheme), to a route that requires authentication. *Note: This option is deprecated—see the warning below.* For more details, refer to the ":ref:`Single Authentication Scheme `" section. * - ``AuthenticationProviderKeys`` - Maps all configured authentication providers, identified by their schemes, to a route that requires authentication. For more details, refer to the ":ref:`Multiple Authentication Schemes `" section. .. warning:: The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_! Use the ``AuthenticationProviderKeys`` array option instead. Note that ``AuthenticationProviderKey`` will be removed in version `25.0`_. For backward compatibility in version `24.1`_, the ``AuthenticationProviderKey`` option takes precedence over the schemes in the ``AuthenticationProviderKeys`` array. If the ``AuthenticationProviderKey`` scheme provider fails, the remaining schemes in the ``AuthenticationProviderKeys`` array will enforce the appropriate authentication providers in the specified order. .. _authentication-scheme: Single Authentication Scheme [#f1]_ ----------------------------------- Option: ``AuthenticationProviderKey`` We map authentication provider to a Route in the configuration e.g. .. code-block:: json "AuthenticationOptions": { "AuthenticationProviderKey": "MyKey", "AllowedScopes": [] } When Ocelot runs it will look at this routes ``AuthenticationProviderKey`` and check that there is an authentication provider registered with the given key. If there isn't then Ocelot will not start up. If there is then the route will use that provider when it executes. If a route is authenticated, Ocelot will invoke whatever scheme is associated with it while executing the authentication middleware. If the request fails authentication, Ocelot returns a HTTP status code `401 Unauthorized `_. .. _authentication-multiple: Multiple Authentication Schemes [#f2]_ -------------------------------------- .. _multiple authentication schemes: https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme#use-multiple-authentication-schemes Option: ``AuthenticationProviderKeys`` In the real world of ASP.NET Core, apps may need to support multiple types of authentication by a single Ocelot app instance. To register `multiple authentication schemes`_ (`authentication provider keys `_) for each appropriate authentication provider, use and develop this abstract configuration of two or more schemes: .. code-block:: csharp var DefaultScheme = JwtBearerDefaults.AuthenticationScheme; // Bearer builder.Services .AddAuthentication() .AddJwtBearer(DefaultScheme, options => { /* JWT setup */ }) // AddJwtBearer, AddCookie, AddIdentityServerAuthentication etc. .AddMyProvider("MyKey", options => { /* Custom auth setup */ }); In this example, the ``MyKey`` and ``Bearer`` schemes represent the keys with which these providers were registered. We then map these schemes to a route in the configuration as shown below. .. code-block:: json "AuthenticationOptions": { "AuthenticationProviderKeys": [ "Bearer", "MyKey" ] // The order matters! "AllowedScopes": [] } Afterward, Ocelot applies all steps that are specified for ``AuthenticationProviderKey`` as :ref:`Single Authentication Scheme `. The order of the keys in an array definition does matter! We use a "First One Wins" authentication strategy. .. _authentication-configuration: Configuration and ``AllowAnonymous`` [#f3]_ ------------------------------------------- To configure *authentication options* uniformly across all static routes, define them in ``GlobalConfiguration`` section using the :ref:`authentication-options-schema`. If *authentication options* are specified in both ``GlobalConfiguration`` and a route (i.e., ``AuthenticationProviderKey`` or ``AuthenticationProviderKeys`` are set), the route-level configuration takes precedence. Excluding a route from global *authentication options* is possible by setting ``AllowAnonymous`` option to ``true``. This prevents the route from requiring authentication, keeping it open and anonymous. In the following example: * The first route is authenticated using the ``MyGlobalKey`` provider's scheme. * The second route uses the ``MyKey`` provider's scheme. * The third route is not authenticated. .. code-block:: json :emphasize-lines: 4, 8-11, 15-17, 22-26 "Routes": [ { // route #1 props... "AuthenticationOptions": {} }, { // route #2 props... "AuthenticationOptions": { "AuthenticationProviderKeys": [ "MyKey" ], "AllowedScopes": [ "Bob" ] } }, { // route #3 props... "AuthenticationOptions": { "AllowAnonymous": true } } ], "GlobalConfiguration": { "BaseUrl": "http://ocelot.net", "AuthenticationOptions": { "RouteKeys": [], // empty -> no grouping, thus opts will apply to all routes "AuthenticationProviderKeys": [ "MyGlobalKey" ], "AllowedScopes": [ "Admin" ] } } .. _break: http://break.do **Note**: Ocelot performs a per-option merging algorithm to combine route and global ``AuthenticationOptions``. If global ``AuthenticationProviderKeys`` are defined together with global ``AllowedScopes``, then route options should be specified as a pair of scheme and scopes; otherwise, a scope should not belong to the global authentication provider. Moreover, the route scopes array entirely overrides the global scopes array, so the two collections are not merged but rather interchangeable. .. _authentication-global-configuration: Global Configuration [#f4]_ --------------------------- Since the global configuration for static routes has already been described above, here are additional details regarding dynamic routes, whose configuration was not supported in versions prior to `24.1`_. Starting with version `24.1`_, global and route *authentication options* for :ref:`Dynamic Routing ` were introduced. These global options may also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`. .. code-block:: json :emphasize-lines: 6-9, 18-22 { "DynamicRoutes": [ { "Key": "R1", // optional "ServiceName": "my-service", "AuthenticationOptions": { "AuthenticationProviderKeys": ["MyKey"], // custom authentication provider "AllowedScopes": ["my-service"] // require authorization with a 'scope' claim set to the value 'my-service' } } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "DownstreamScheme": "http", "ServiceDiscoveryProvider": { // required section for dynamic routing }, "AuthenticationOptions": { "RouteKeys": [], // or null, no grouping, thus opts apply to all dynamic routes "AuthenticationProviderKeys": ["Bearer"], // use a global JWT bearer auth provider for all discovered services "AllowedScopes": ["oc-admin"] // require the global 'scope' claim to gain access to all discovered services } } } In this configuration, an ``oc-admin`` scope authorization is applied to all implicit dynamic routes by the global ``Bearer`` JWT signing service. However, for the “my-service” service, authorization with the ``my-service`` scope is applied, and authentication is provided by another source of tokens named ``MyKey``. .. note:: 1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``AuthenticationOptions``, the global options will apply to all routes. If the array contains route keys, it defines a single group of routes to which the global options apply. Routes excluded from this group must specify their own route-level ``AuthenticationOptions``. 2. Prior to version `24.1`_, global and dynamic route ``AuthenticationOptions`` were not available. Starting with version `24.1`_, global configuration is supported for both static and dynamic routes. .. _authentication-allowed-scopes: Allowed Scopes -------------- | Option: ``AllowedScopes`` | Middleware: :ref:`authorization-middleware` To set up authorization by scopes from the ``AllowedScopes`` collection, after successful authentication by the middleware and after claims have been transformed, the authorization middleware in Ocelot retrieves all user claims (from the token) of the '``scope``' type and ensures that the user has at least one of the scopes in the list. This provides a way to restrict access to a route on a per-scope basis. .. note:: [#f5]_ Depending on the authentication provider, incoming tokens embed the '``scope``' claim value in the body either as an array or as a single space-separated string of multiple values. For instance, :ref:`authentication-identity-server` use an array, whereas most :ref:`authentication-jwt-tokens` providers generate a space-separated list of scopes, in accordance with `RFC 8693`_, as stated in section "`4.2. "scope" (Scopes) Claim`_". Since version `24.1`_, Ocelot supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the ``scope`` claim in the ``ScopesAuthorizer`` service, also known as the ``IScopesAuthorizer`` service in the DI container. .. note:: Starting with version `24.1`_, specifying global *allowed scopes* is exclusively supported. Be cautious when overriding the global ``AllowedScopes`` array with a route-level ``AllowedScopes`` array; a combination of the route scheme (``AuthenticationProviderKeys`` array) and its *allowed scopes* might be required, since new *allowed scopes* could belong to another authentication provider's security model. For more details, refer to the ":ref:`Configuration and AllowAnonymous `" and ":ref:`Global Configuration `" sections. .. _authentication-jwt-tokens: JWT Tokens ---------- If you want to authenticate using JWT tokens maybe from a provider like `Auth0 `_, you can register your authentication middleware as normal e.g. .. code-block:: csharp builder.Services .AddAuthentication() .AddJwtBearer("Auth0", options => { options.Authority = "test"; options.Audience = "test"; }); builder.Services .AddOcelot(builder.Configuration); Then map the authentication provider key to a route in your configuration e.g. .. code-block:: json "AuthenticationOptions": { "AuthenticationProviderKeys": ["Auth0"], } **JWT Tokens Docs** * Microsoft Learn: `Authentication and authorization in minimal APIs `_ * Andrew Lock | .NET Escapades: `A look behind the JWT bearer authentication middleware in ASP.NET Core `_ .. _authentication-identity-server: Identity Server Bearer Tokens ----------------------------- In order to use `IdentityServer `_ bearer tokens, register your IdentityServer services as usual in `Program`_ with a `scheme`_ (key). If you don't understand how to do this, please consult the IdentityServer `documentation `_. .. code-block:: csharp Action options = o => { o.Authority = "https://whereyouridentityserverlives.com"; // ... }; builder.Services .AddAuthentication() .AddJwtBearer("IS4", options); builder.Services .AddOcelot(builder.Configuration); Then map the authentication provider key to a route in your configuration e.g. .. code-block:: json "AuthenticationOptions": { "AuthenticationProviderKeys": ["IS4"], } Auth0 by Okta ------------- Yet another identity provider by `Okta `_, see `Auth0 Developer Resources `_. Add the following, at minimum, to your startup `Program`_: .. code-block:: csharp builder.Services .AddAuthentication() .AddJwtBearer("Okta", o => { var conf = builder.Configuration; o.Audience = conf["Authentication:Okta:Audience"]; // Okta Authorization server Audience o.Authority = conf["Authentication:Okta:Server"]; // Okta Authorization Issuer URI URL e.g. https://{subdomain}.okta.com/oauth2/{authidentifier} }); builder.Services .AddOcelot(builder.Configuration); var app = builder.Build(); await app .UseAuthentication() .UseOcelot(); await app.RunAsync(); In order to get Ocelot to view the scope claim from Okta properly, you have to add the following to map the default Okta ``scp`` claim to ``scope``: .. code-block:: csharp // Map Okta "scp" to "scope" claims instead of http://schemas.microsoft.com/identity/claims/scope to allow Ocelot to read/verify them JsonWebTokenHandler.DefaultInboundClaimTypeMap.Remove("scp"); JsonWebTokenHandler.DefaultInboundClaimTypeMap.Add("scp", "scope"); **Okta Notes** 1. Issue `446`_ contains some code and examples that might help with Okta integration. 2. Here is documentation for better clarity on claims mapping: `Mapping, customizing, and transforming claims in ASP.NET Core`_. 3. It is highly advisable to read and understand the :ref:`authentication-warnings` related to the critical changes in authentication when utilizing .NET 8. .. _authentication-warnings: Warnings -------- .. warning:: .NET 8 introduced a breaking change where ``JwtSecurityToken`` was replaced with ``JsonWebToken`` to enhance performance and reliability. Consequently, their handlers were changed ``JwtSecurityTokenHandler`` to ``JsonWebTokenHandler``. For a complete understanding of .NET 8 breaking change related to JWT tokens, please refer to the Microsoft Learn documentation: "`Security token events return a JsonWebToken `__". Links ----- .. _Mapping, customizing, and transforming claims in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-9.0 * Microsoft Learn: `Overview of ASP.NET Core authentication `_ * Microsoft Learn: `Authorize with a specific scheme in ASP.NET Core `_ * Microsoft Learn: `Policy schemes in ASP.NET Core `_ * Microsoft Learn: `Mapping, customizing, and transforming claims in ASP.NET Core`_ * Microsoft .NET Blog: `ASP.NET Core Authentication with IdentityServer4 `_ Roadmap ------- Nothing is currently in the stack, but the Ocelot team is rethinking a new version of the ":doc:`../features/administration`" feature, which is closely dependent on authentication. We invite you to add more examples if you have integrated with other identity providers and the integration solution is working. Please open a "`Show and tell `_" discussion in the repository. """" .. [#f1] The ":ref:`Single Authentication Scheme `" feature has been an Ocelot artifact for ages. Use the ``AuthenticationProviderKeys`` property instead of ``AuthenticationProviderKey`` one. We support this ``[Obsolete]`` property for backward compatibility and migration reasons. In future releases, the property may be removed as a breaking change. .. [#f2] The ":ref:`Multiple Authentication Schemes `" feature was requested in issues `740`_, `1580`_ and delivered as a part of `23.0`_ release. .. [#f3] The global ":ref:`Configuration and AllowAnonymous `" feature for static routes was requested in issues `842`_ and `1414`_, implemented in pull request `2114`_, and officially released in version `24.1`_. .. [#f4] The ":ref:`Global Configuration `" feature for dynamic routes was requested in issues `585`_ and `2316`_, implemented in pull request `2336`_, and released in version `24.1`_. .. [#f5] The ":ref:`authentication-allowed-scopes`" feature fully supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the ``scope`` claim in the ``ScopesAuthorizer`` service, which is part of the :ref:`authorization-middleware`. Refer to section `4.2. "scope" (Scopes) Claim`_. This enhancement was requested in bug `913`_, fixed in pull request `1478`_, and the patch was rolled out as part of the `24.1`_ release. .. _RFC 8693: https://datatracker.ietf.org/doc/html/rfc8693 .. _4.2. "scope" (Scopes) Claim: https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim .. _446: https://github.com/ThreeMammals/Ocelot/issues/446 .. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. _740: https://github.com/ThreeMammals/Ocelot/issues/740 .. _842: https://github.com/ThreeMammals/Ocelot/issues/842 .. _913: https://github.com/ThreeMammals/Ocelot/issues/913 .. _1414: https://github.com/ThreeMammals/Ocelot/issues/1414 .. _1478: https://github.com/ThreeMammals/Ocelot/pull/1478 .. _1580: https://github.com/ThreeMammals/Ocelot/issues/1580 .. _2114: https://github.com/ThreeMammals/Ocelot/pull/2114 .. _2316: https://github.com/ThreeMammals/Ocelot/issues/2316 .. _2336: https://github.com/ThreeMammals/Ocelot/pull/2336 .. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13 ================================================ FILE: docs/features/authorization.rst ================================================ Authorization ============= Ocelot supports claims based authorization which is run post authentication. This means if you have a route you want to authorize, you can add the following to your route configuration: .. code-block:: json "RouteClaimsRequirement": { "UserType": "registered" } In this example, when the :ref:`authorization-middleware` is called, Ocelot will check to see if the user has the claim type ``UserType`` and if the value of that claim is ``"registered"``. If it isn't then the user will not be authorized and the response will be `403 Forbidden `_. .. _authorization-middleware: Authorization Middleware ------------------------ The `AuthorizationMiddleware `_ is built-in into Ocelot pipeline. | Previous private: ``ClaimsToClaimsMiddleware`` | Previous public: ``PreAuthorizationMiddleware`` | **This**: ``AuthorizationMiddleware`` | Next private: ``ClaimsToHeadersMiddleware`` | Next public: ``PreQueryStringBuilderMiddleware`` .. role:: htm(raw) :format: html So, the closest middlewares are in order of calling: ``ClaimsToClaimsMiddleware`` :htm:`→` ``PreAuthorizationMiddleware`` :htm:`→` **AuthorizationMiddleware** :htm:`→` ``ClaimsToHeadersMiddleware`` :htm:`→` ``PreQueryStringBuilderMiddleware`` As you may know from the :doc:`../features/middlewareinjection` chapter, the Authorization middleware can be overridden like this: .. code-block:: csharp var app = builder.Build(); await app.UseOcelot(new OcelotPipelineConfiguration { AuthorizationMiddleware = async (context, next) => { await next.Invoke(); } }); await app.RunAsync(); **Note!** Do this in very rare cases, because overriding the Authorization middleware means you will lose claims and scopes authorizer through the ``RouteClaimsRequirement`` property of the route. Another option is preparing before the actual authorization in ``PreAuthorizationMiddleware``, which is public and open to overriding. .. code-block:: csharp await app.UseOcelot(new OcelotPipelineConfiguration { PreAuthorizationMiddleware = async (context, next) => { // Do whatever you want here await next.Invoke(); // next is AuthorizationMiddleware } }); ================================================ FILE: docs/features/caching.rst ================================================ .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs Caching ======= [#f1]_ Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. Users can also clear the cache for a specific region by using Ocelot's :ref:`administration-api`. Ocelot utilizes some very rudimentary caching at the moment provider by the `CacheManager `_ project. This is an amazing project that is solving a lot of caching problems. We would recommend using this package to cache with Ocelot. The following example shows how to add *CacheManager* to Ocelot so that you can do output caching. Install ------- First of all, add the following `Ocelot.Cache.CacheManager `_ package: .. code-block:: powershell Install-Package Ocelot.Cache.CacheManager This will give you access to the Ocelot cache manager extension methods. The second step is to add the following to your `Program`_: .. code-block:: csharp using Ocelot.Cache.CacheManager; builder.Services .AddOcelot(builder.Configuration) .AddCacheManager(x => x.WithDictionaryHandle()); ``CacheOptions`` Schema ----------------------- .. _FileCacheOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileCacheOptions.cs Class: `FileCacheOptions`_ The following is the full *caching* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`. Not all of these options need to be configured; however, the ``TtlSeconds`` option is mandatory. .. code-block:: json "CacheOptions": { "TtlSeconds": 1, // nullable integer "Region": "", // string "Header": "", // string "EnableContentHashing": false // nullable boolean } .. list-table:: :widths: 25 75 :header-rows: 1 * - *Option* - *Description* * - ``TtlSeconds`` - Time-To-Live (TTL) in seconds for the cached downstream response, i.e., the absolute expiration timeout starting from when the item is added to the cache. This option is required. If undefined, it defaults to 0 (zero), which disables caching. * - ``Region`` - Specifies the cache region to be cleared via Ocelot's :ref:`administration-api`. See: ``DELETE {adminPath}/outputcache/{region}`` * - ``Header`` - Specifies the header name used for native Ocelot caching control, defaulting to the special ``OC-Cache-Control`` header. If the header is present, its value is included in the cache key constructed by the ``ICacheKeyGenerator`` service. Varying header values result in different cache keys, effectively invalidating the cache. * - ``EnableContentHashing`` - Toggles inclusion of request body hashing in the cache key. Disabled by default (``false``) due to potential performance impact. Recommended for POST/PUT routes where request body affects response. Refer to the :ref:`EnableContentHashing option ` section. The actual ``CacheOptions`` schema with all the properties can be found in the C# `FileCacheOptions`_ class. .. _caching-configuration: Configuration ------------- Finally, in order to use caching on a route in your route configuration add these sections: .. code-block:: json "CacheOptions": { "TtlSeconds": 15, "Region": "europe-central", "Header": "OC-Cache-Control", "EnableContentHashing": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc. }, // Warning! FileCacheOptions section is deprecated! -> use CacheOptions "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", "Header": "OC-Cache-Control", "EnableContentHashing": false // my route has GET verb only, assigning 'true' for requests with body: POST, PUT etc. } * In this example, ``TtlSeconds`` is set to 15, which means the cache will expire 15 seconds after the response is stored. * The ``Region`` property specifies a cache region. Cache entries within a region can be cleared by calling Ocelot's :ref:`administration-api`. * If a header name is defined in the ``Header`` property, its value is retrieved from the ``HttpRequest`` headers. If the header is present, its value is included in the cache key constructed by the ``ICacheKeyGenerator`` service. Varying header values result in different cache keys, effectively invalidating the cache. * Finally, ``EnableContentHashing`` is disabled due to the current route using the ``GET`` verb, which does not include a request body. .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13 .. warning:: According to the static :ref:`config-route-schema`, the ``FileCacheOptions`` section has been deprecated! The `old schema `_ ``FileCacheOptions`` section is deprecated in version `24.1`_! Use ``CacheOptions`` instead of ``FileCacheOptions``! Note that ``FileCacheOptions`` will be removed in version `25.0`_! For backward compatibility in version `24.1`_, the ``FileCacheOptions`` section takes precedence over the ``CacheOptions`` section. .. _caching-enablecontenthashing-option: ``EnableContentHashing`` option [#f2]_ -------------------------------------- Previously, in versions prior to `23.0`_, the request body was used to compute the cache key. However, due to potential performance issues arising from request body hashing, it has been disabled by default. Clearly, this constitutes a breaking change and presents challenges for users who require cache key calculations that consider the request body (e.g., for the POST method). To address this issue, it is recommended to enable the option either at the route level or globally in the ":ref:`Global Configuration `" section: .. code-block:: json "CacheOptions": { // ... "EnableContentHashing": true } .. rubric:: Ocelot Team Recommendation Although the community raised concerns about backward compatibility in issue `2234`_, Ocelot team maintains that *caching* performance takes precedence over backward compatibility when migrating from versions prior to `23.0`_. The proposed option clarifies that ``POST`` requests should **not** be cached; only ``GET`` requests are eligible for caching. Therefore, ``POST`` and ``GET`` verbs must be separated into distinct routes: * POST routes with *caching* disabled * GET routes with *caching* enabled .. _caching-global-configuration: Global Configuration [#f3]_ --------------------------- Copying route-level properties for each static route is no longer necessary, as version `23.3`_ allows these values to be set in the ``GlobalConfiguration`` section. This convenience applies to ``Header`` and ``Region`` as well. However, if no global ``TtlSeconds`` value is defined, this option must still be explicitly set per route to enable caching. As a result, the final configuration for static routes might look like: .. code-block:: json :emphasize-lines: 5-7, 12, 18-21 { "Routes": [ { "Key": "R0", // optional "CacheOptions": { "TtlSeconds": 60 // 1-minute short-term caching }, // ... }, { "Key": "R1", // this route is part of a group "CacheOptions": {}, // optional due to grouping // ... } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "CacheOptions": { "RouteKeys": ["R1",], // if undefined or empty array, opts will apply to all routes "TtlSeconds": 300 // enable global caching for a duration of 5 minutes }, // ... } } Dynamic routes were not supported in versions prior to `24.1`_. Starting with version `24.1`_, global *cache options* for :ref:`Dynamic Routing ` were introduced. These global options may also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`. .. code-block:: json :emphasize-lines: 6-8, 17-20 { "DynamicRoutes": [ { "Key": "", // optional "ServiceName": "my-service", "CacheOptions": { "TtlSeconds": 60 // 1-minute short-term caching } } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "DownstreamScheme": "http", "ServiceDiscoveryProvider": { // required section for dynamic routing }, "CacheOptions": { "RouteKeys": [], // or null, no grouping, thus opts apply to all dynamic routes "TtlSeconds": 300 // enable global caching for a duration of 5 minutes } } } In this configuration, a 5-minute *caching* duration is applied to all implicit dynamic routes. However, for the "my-service" service, the *caching* TTL has been explicitly reduced from 5 minutes to 1 minute. .. note:: 1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``CacheOptions``, the global options will apply to all routes. If the array contains route keys, it defines a single group of routes to which the global options apply. Routes excluded from this group must specify their own route-level ``CacheOptions``. 2. Prior to version `23.3`_, global ``CacheOptions`` were not available. Starting with version `24.1`_, global configuration is supported for both static and dynamic routes. .. Sample .. ----- .. If you look at the example `here `_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method. .. You can use any settings supported by the **CacheManager** package and just pass them in. Custom Caching -------------- If you want to add your own caching method, implement the following interfaces and register them in DI e.g. .. code-block:: csharp builder.Services .AddSingleton, MyCache>(); * ``IOcelotCache`` this is for output caching. * ``IOcelotCache`` this is for caching the file configuration if you are calling something remote to get your config such as Consul. Roadmap ------- Please dig into the Ocelot source code to find more. We would really appreciate it if anyone wants to implement `Redis `_, `Memcached `_ etc. Please, open a new `Show and tell `_ thread in `Discussions `_ space of the repository. """" .. [#f1] Historically, *Caching* is one of Ocelot's earliest features, first introduced in version `1.1`_ on February 2, 2017, the initial release of Ocelot. The "Clear cache region via :ref:`administration-api`" feature was first delivered in pull request `109`_ and released in version `1.4.8`_. .. [#f2] The ":ref:`EnableContentHashing option `" feature was requested in issue `2059`_ and released in version `23.3`_. .. [#f3] :ref:`Global Configuration ` for static routes was first introduced in pull request `2058`_ and released in version `23.3`_. Support for dynamic routes was added in pull request `2331`_ and delivered in version `24.1`_. .. _109: https://github.com/ThreeMammals/Ocelot/pull/109 .. _2058: https://github.com/ThreeMammals/Ocelot/pull/2058 .. _2059: https://github.com/ThreeMammals/Ocelot/issues/2059 .. _2234: https://github.com/ThreeMammals/Ocelot/issues/2234 .. _2331: https://github.com/ThreeMammals/Ocelot/pull/2331 .. _1.1: https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 .. _1.4.8: https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.8 .. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 ================================================ FILE: docs/features/claimstransformation.rst ================================================ Claims Transformation ===================== Ocelot allows the user to access claims and transform them into headers, query string parameters, other claims and change downstream paths. This is only available once a user has been authenticated. After the user is authenticated, we run the claims to claims transformation middleware (see the `ClaimsToClaimsMiddleware `_ class). This allows the user to transform claims before the authorization middleware is called. After the user is authorized, we call the claims to headers middleware (see the `ClaimsToHeadersMiddleware `_ class), then the claims to query string parameters middleware (see the `ClaimsToQueryStringMiddleware `_ class), and finally the claims to downstream path middleware (see the `ClaimsToDownstreamPathMiddleware `_ class). The syntax for performing the transforms is the same for each process. In the route configuration, a JSON dictionary is added with a specific name either ``AddClaimsToRequest``, ``AddHeadersToRequest``, ``AddQueriesToRequest``, or ``ChangeDownstreamPathTemplate``. **Note**: This syntax is not ideal. So any suggestions are welcome... Within this dictionary the entries specify how Ocelot should transform things! The key to the dictionary is going to become the key of either a claim, header or query parameter. In the case of ``ChangeDownstreamPathTemplate``, the key must be also specified in the ``DownstreamPathTemplate``, in order to do the transformation. The value of the entry is parsed to logic that will perform the transform. First of all, a dictionary accessor is specified e.g. ``Claims[CustomerId]``. This means we want to access the claims and get the ``CustomerId`` claim type. Next is a "greater than" ``>`` symbol which is just used to split the string. The next entry is either value or value with an indexer. If value is specified, Ocelot will just take the value and add it to the transform. If the value has an indexer, Ocelot will look for a delimiter which is provided after another "greater than" ``>`` symbol. Ocelot will then split the value on the delimiter and add whatever was at the index requested to the transform. Claims to Claims ---------------- Below is an example configuration that will transform claims to claims .. code-block:: json "AddClaimsToRequest": { "UserType": "Claims[sub] > value[0] > |", "UserId": "Claims[sub] > value[1] > |" } This shows a transforms where Ocelot looks at the users ``sub`` claim and transforms it into ``UserType`` and ``UserId`` claims. Assuming the ``sub`` looks like this ``usertypevalue|useridvalue``. Claims to Headers ----------------- Below is an example configuration that will transform claims to headers .. code-block:: json "AddHeadersToRequest": { "CustomerId": "Claims[sub] > value[1] > |" } This shows a transform where Ocelot looks at the users ``sub`` claim and transforms it into a ``CustomerId`` header. Assuming the ``sub`` looks like this ``usertypevalue|useridvalue``. Claims to Query String Parameters --------------------------------- Below is an example configuration that will transform claims to query string parameters .. code-block:: json "AddQueriesToRequest": { "LocationId": "Claims[LocationId] > value", } This shows a transform where Ocelot looks at the users ``LocationId`` claim and add it as a query string parameter to be forwarded onto the downstream service. Claims to Downstream Path ------------------------- Below is an example configuration that will transform claims to downstream path custom placeholders: .. code-block:: json "UpstreamPathTemplate": "/api/users/me/{everything}", "DownstreamPathTemplate": "/api/users/{userId}/{everything}", "ChangeDownstreamPathTemplate": { "userId": "Claims[sub] > value[1] > |", } This shows a transform where Ocelot looks at the users ``userId`` claim and substitutes the value to the ``{userId}`` placeholder specified in the ``DownstreamPathTemplate``. Take into account that the key specified in the ``ChangeDownstreamPathTemplate`` must be the same than the placeholder specified in the ``DownstreamPathTemplate``. **Note**: If a key specified in the ``ChangeDownstreamPathTemplate`` does not exist as a placeholder in ``DownstreamPathTemplate``, it will fail at runtime returning an error in the response. ================================================ FILE: docs/features/configuration.rst ================================================ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Configuration/Program.cs .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs .. _Consul: https://www.consul.io/ .. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv Configuration ============= An example configuration can be found here in `ocelot.json`_. There are two major sections to the configuration: an array of ``Routes`` and a ``GlobalConfiguration`` sections: .. code-block:: json { "Routes": [], "GlobalConfiguration": {} } From the :doc:`../introduction/gettingstarted` chapter and its :ref:`getstarted-configuration` section, you may already know that there are four total configuration sections: .. code-block:: json { "Routes": [], // static routes "DynamicRoutes": [], "Aggregates": [], // BFF "GlobalConfiguration": {} } .. list-table:: :widths: 25 75 :header-rows: 1 * - *Section* - *Description* * - ``Routes`` with :ref:`config-route-schema` - The static objects that tell Ocelot how to treat an upstream request. Once static routes have been loaded during gateway startup, in general, they cannot be changed during the lifetime of the app instance, with a few exceptional use cases. * - ``DynamicRoutes`` with :ref:`config-dynamic-route-schema` - This section enables dynamic routing when using a :doc:`../features/servicediscovery` provider. Please refer to the :ref:`routing-dynamic` docs for more details. * - ``Aggregates`` with :ref:`config-aggregate-route-schema` - This section allows specifying aggregated routes that compose multiple normal routes and map their responses into one JSON object. It allows you to start implementing a *Back-end For a Front-end* (BFF) type architecture with Ocelot. Please refer to the :doc:`../features/aggregation` chapter for more details. * - ``GlobalConfiguration`` with :ref:`config-global-configuration-schema` - This section is a bit hacky and allows overrides of static route-specific settings. It is useful if you do not want to manage lots of route-specific settings. To fully understand all configuration capabilities, we recommend reading all sections below. .. _config-route-schema: Route Schema ------------ .. _FileRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRoute.cs Class: `FileRoute`_ Here is the complete route configuration, also known as the *"route schema,"* of top-level properties. You do not need to set all of these things, but this is everything that is available at the moment. .. code-block:: json { "AddClaimsToRequest": {}, // dictionary "AddHeadersToRequest": {}, // dictionary "AddQueriesToRequest": {}, // dictionary "AuthenticationOptions": {}, // object "ChangeDownstreamPathTemplate": {}, // dictionary "DangerousAcceptAnyServerCertificateValidator": false, "DelegatingHandlers": [], // array of strings "DownstreamHeaderTransform": {}, // dictionary "DownstreamHostAndPorts": [], // array of FileHostAndPort "DownstreamHttpMethod": "", "DownstreamHttpVersion": "", "DownstreamHttpVersionPolicy": "", "DownstreamPathTemplate": "", "DownstreamScheme": "", "CacheOptions": {}, // object "FileCacheOptions": {}, // deprecated! -> use CacheOptions "HttpHandlerOptions": {}, // object "Key": "", "LoadBalancerOptions": {}, // object "Metadata": {}, // dictionary "Priority": 1, // integer "QoSOptions": {}, // object "RateLimitOptions": {}, // object "RequestIdKey": "", "RouteClaimsRequirement": {}, // dictionary "RouteIsCaseSensitive": false, "SecurityOptions": {}, // object "ServiceName": "", "ServiceNamespace": "", "Timeout": 0, // nullable integer "UpstreamHeaderTemplates": {}, // dictionary "UpstreamHeaderTransform": {}, // dictionary "UpstreamHost": "", "UpstreamHttpMethod": [], // array of strings "UpstreamPathTemplate": "" }, The actual route schema with all the properties can be found in the C# `FileRoute`_ class. **Note**: The `old schema `__ ``FileCacheOptions`` section is deprecated in version `24.1`_! Use ``CacheOptions`` instead of ``FileCacheOptions``! Note that ``FileCacheOptions`` will be removed in version `25.0`_! For backward compatibility in version `24.1`_, the ``FileCacheOptions`` section takes precedence over the ``CacheOptions`` section. .. _config-dynamic-route-schema: Dynamic Route Schema -------------------- .. _FileDynamicRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileDynamicRoute.cs Class: `FileDynamicRoute`_ Here is the complete dynamic route configuration, also known as the *"dynamic route schema,"* of top-level properties. .. code-block:: json { "AuthenticationOptions": {}, "CacheOptions": {}, "DownstreamHttpVersion": "", "DownstreamHttpVersionPolicy": "", "HttpHandlerOptions": {}, "LoadBalancerOptions": {}, "Metadata": {}, // dictionary "QoSOptions": {}, "RateLimitRule": {}, // deprecated! -> use RateLimitOptions "RateLimitOptions": {}, "ServiceName": "", "ServiceNamespace": "", "Timeout": 0 // nullable integer } The actual dynamic route schema with all the properties can be found in the C# `FileDynamicRoute`_ class. **Note 1**: The `old schema `_ ``RateLimitRule`` section is deprecated in version `24.1`_! Use ``RateLimitOptions`` instead of ``RateLimitRule``! Note that ``RateLimitRule`` will be removed in version `25.0`_! For backward compatibility in version `24.1`_, the ``RateLimitRule`` section takes precedence over the ``RateLimitOptions`` section. **Note 2**: The following options were not supported in versions prior to `24.1`_ for overriding globally configured options: ``AuthenticationOptions``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, ``ServiceNamespace``, and ``Timeout``. Starting with version `24.1`_, both global and route-level options for :ref:`Dynamic Routing ` were introduced. For a clearer understanding of the changes, refer to the `previous schema (version 24.0) `_. .. _config-aggregate-route-schema: Aggregate Route Schema ---------------------- .. _FileAggregateRoute: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileAggregateRoute.cs Class: `FileAggregateRoute`_ Here is the complete aggregated route configuration, also known as the *"aggregate route schema,"* of top-level properties. .. code-block:: json { "Aggregator": "", "Priority": 1, // integer "RouteIsCaseSensitive": false, "RouteKeys": [], // array of strings "RouteKeysConfig": [], // array of AggregateRouteConfig "UpstreamHeaderTemplates": {}, // dictionary "UpstreamHost": "", "UpstreamHttpMethod": [], // array of strings "UpstreamPathTemplate": "" } The actual aggregated route schema with all the properties can be found in the C# `FileAggregateRoute`_ class. .. _config-global-configuration-schema: Global Configuration Schema --------------------------- .. _FileGlobalConfiguration: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs Class: `FileGlobalConfiguration`_ Here is the complete global configuration, also known as the *"global configuration schema,"* of top-level properties. .. code-block:: json { "AuthenticationOptions": {}, "BaseUrl": "", "CacheOptions": {}, "DownstreamHeaderTransform": {}, // dictionary "DownstreamHttpVersion": "", "DownstreamHttpVersionPolicy": "", "DownstreamScheme": "", "HttpHandlerOptions": {}, "LoadBalancerOptions": {}, "Metadata": {}, // dictionary "MetadataOptions": {}, "QoSOptions": {}, "RateLimitOptions": {}, "RequestIdKey": "", "SecurityOptions": {}, "ServiceDiscoveryProvider": {}, "Timeout": 0, // nullable integer "UpstreamHeaderTransform": {} // dictionary } The actual global configuration schema with all the properties can be found in the C# `FileGlobalConfiguration`_ class. **Note 1**: The following global options were not supported in versions prior to `24.1`_ for overriding in the :ref:`config-dynamic-route-schema`: ``AuthenticationOptions``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, and ``Timeout``. Moreover, these global options were not available in versions prior to `24.1`_ for static routes, as stated in issue `585`_. Starting with version `24.1`_, both static and dynamic route *global* options are fully supported. For a clearer understanding of the changes, refer to the :ref:`config-dynamic-route-schema` and related notes. **Note 2**: The ``DownstreamHeaderTransform`` and ``UpstreamHeaderTransform`` global options were introduced in version `24.1`_, but they are available only for static routes. .. _config-overview: Configuration Overview ---------------------- :doc:`../features/dependencyinjection` of the *Configuration* feature in Ocelot allows you to extend, manage, and build Ocelot Core *configuration* **before** the stage of building ASP.NET Core services. To configure the Ocelot Core and services, use the following abstract program-structure, which must be presented in your `Program`_: 1. **Create application builder**: The ``Microsoft.AspNetCore.Builder.WebApplication`` has three overloaded versions of the ``CreateBuilder()`` methods. Our recommendation is to utilize arguments possibly coming from terminal sessions into an app host; thus, use the ``CreateBuilder(args)`` method. .. code-block:: csharp var builder = WebApplication.CreateBuilder(args); 2. **Set up the configuration builder**: Utilize the ``WebApplicationBuilder.Configuration`` property, which returns a ``ConfigurationManager`` object implementing the target ``IConfigurationBuilder`` interface. .. code-block:: csharp builder.Configuration.AddOcelot(...); 3. **Forward configuration to the Ocelot builder**: The ``Ocelot.DependencyInjection.ServiceCollectionExtensions`` class has three overloaded versions of the ``AddOcelot(IServiceCollection)`` methods, which return an ``IOcelotBuilder`` object. .. code-block:: csharp builder.Services.AddOcelot(builder.Configuration); 4. **Finish the app setup**, add middlewares, and finally run the application: Let's write the final algorithm. .. code-block:: csharp var builder = WebApplication.CreateBuilder(args); // step 1 builder.Configuration.AddOcelot(...); // step 2 builder.Services.AddOcelot(builder.Configuration); // step 3 // Step 4 var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); For comprehensive documentation of configuration DI-extensions, please refer to the :ref:`di-configuration-overview` section in the :doc:`../features/dependencyinjection` chapter. Multiple Environments --------------------- Like any other ASP.NET Core project Ocelot supports configuration file names such as ``appsettings.dev.json``, ``appsettings.test.json`` etc. In order to implement this add the following to you: .. code-block:: csharp :emphasize-lines: 4,5,7 var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddJsonFile("ocelot.json") // primary config file .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json"); builder.Services .AddOcelot(builder.Configuration); Ocelot will now use the environment specific configuration and fall back to `ocelot.json`_ if there isn't one. Another version of the configuration above, which is based on configuration providers, is the following: .. code-block:: csharp :emphasize-lines: 4,6,7,9 var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot() // single ocelot.json file without environment one // or .AddOcelot(builder.Environment) .AddJsonFile($"ocelot.{builder.Environment.EnvironmentName}.json"); builder.Services .AddOcelot(builder.Configuration); You also need to set the corresponding ``ASPNETCORE_ENVIRONMENT`` variable. **Note 1**: More info on configuration can be found in the ASP.NET Core documentation: * `Use multiple environments in ASP.NET Core `_ * `Configuration in ASP.NET Core `_ **Note 2**: Calling the following configuration methods is rudimentary in ASP.NET Core because of internal encapsulation in the default builder, aka ``CreateBuilder(args)`` method. .. code-block:: csharp :emphasize-lines: 3,4,5 var builder = WebApplication.CreateBuilder(args); builder.Configuration .AddJsonFile("appsettings.json", true, true) // not required .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", true, true) // not required .AddEnvironmentVariables() // not required // ... This is explained in the `Default application configuration sources `_ docs; thus, remove these optional methods. .. _config-merging-files: Merging Files [#f1]_ -------------------- **Sample**: `Ocelot.Samples.Configuration `_ This feature allows users to have multiple configuration files to make managing large configurations easier. Rather than directly adding the configuration e.g., using ``AddJsonFile("ocelot.json")``, you can achieve the same result by invoking ``AddOcelot()`` as shown below: .. code-block:: csharp :emphasize-lines: 3 builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(builder.Environment); // will skip environment file In this scenario, Ocelot will look for any files that match the pattern ``^ocelot\.(.*?)\.json$`` as the regular expression and then merge these together. The environment file will be skipped aka ``ocelot.{builder.Environment.EnvironmentName}.json``. If you want to set the ``GlobalConfiguration`` property, you must have a file called ``ocelot.global.json``. The way Ocelot merges the files is basically load them, loop over them, skip environment file, add any ``Routes``, add any ``AggregateRoutes`` and if the file is called ``ocelot.global.json`` add the ``GlobalConfiguration`` aswell as any ``Routes`` or ``AggregateRoutes``. Ocelot will then save the merged configuration to a file called `ocelot.json`_ and this will be used as the source of truth while Ocelot is running. **Note 1**: Currently, validation occurs only during the final merging of configurations in Ocelot. It's essential to be aware of this when troubleshooting issues. We recommend thoroughly inspecting the contents of the ``ocelot.json`` file if you encounter any problems. **Note 2**: The Merging feature is operational only during the application's startup. Consequently, the merged configuration in ``ocelot.json`` remains static post-merging and startup. Once the Ocelot application has started, you cannot call the ``AddOcelot`` method, nor can you employ the merging feature within ``AddOcelot``. If you still require on-the-fly updating of the primary configuration file, ``ocelot.json``, please refer to the :ref:`config-react-to-changes` section. Additionally, note that merging partial configuration files (such as ``ocelot.*.json``) on the fly using :doc:`../features/administration` API is not currently implemented. **Note 3**: An alternative to static merged configurations could be the construction of the ``FileConfiguration`` object before passing it as an argument to the :ref:`di-configuration-addocelot-methods` method. Refer to the :ref:`config-build-from-scratch` subsection for details. Keep files in a folder ^^^^^^^^^^^^^^^^^^^^^^ You can also give Ocelot a specific path to look in for the configuration files as shown below: .. code-block:: csharp :emphasize-lines: 3 builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot("/my/folder", builder.Environment); // happy path Ocelot needs the ``builder.Environment`` so it knows to exclude any environment-specific files from the merging algorithm, such as ``ocelot.{builder.Environment.EnvironmentName}.json``. .. _config-merging-tomemory: Merging files to memory [#f2]_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default, Ocelot writes the merged configuration to disk as `ocelot.json`_ (the primary configuration file) by adding the file to the ASP.NET configuration provider. If your web server lacks write permissions for the configuration folder, you can instruct Ocelot to use the merged configuration directly from memory. Here's how: .. code-block:: csharp :emphasize-lines: 5 builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) // It implicitly calls ASP.NET AddJsonStream extension method for IConfigurationBuilder // .AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))); .AddOcelot(builder.Environment, MergeOcelotJson.ToMemory); This feature proves exceptionally valuable in cloud environments like Azure, AWS, and GCP, especially when the app lacks sufficient write permissions to save files. Furthermore, within Docker container environments, permissions can be scarce, necessitating substantial DevOps efforts to enable file write operations. Therefore, save time by leveraging this feature! Reload On Change ---------------- Ocelot supports reloading the JSON configuration file on change. For instance, the following will recreate Ocelot internal configuration when the `ocelot.json`_ file is updated manually: .. code-block:: csharp :emphasize-lines: 3 builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddJsonFile("ocelot.json", optional: false, reloadOnChange: true) // ASP.NET framework version .. _break: http://break.do **Note**: Starting from version `23.2`_, most :ref:`di-configuration-addocelot-methods` include optional ``bool?`` arguments, specifically ``optional`` and ``reloadOnChange``. Therefore, you have the flexibility to provide these arguments when invoking the native `AddJsonFile method `_ during the final configuration step (see `AddOcelotJsonFile `_ implementation). We recommend using the :ref:`di-configuration-addocelot-methods` to control reloading, rather than relying on the framework's ``AddJsonFile`` method. For example: .. code-block:: csharp :emphasize-lines: 4,13-16 // Old solution based on native framework functionality builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddJsonFile(ConfigurationBuilderExtensions.PrimaryConfigFile, optional: false, reloadOnChange: true); var config = builder.Configuration; var env = builder.Environment; var mergeTo = MergeOcelotJson.ToFile; // ToMemory var folder = "/My/folder"; var configuration = new FileConfiguration(); // read from anywhere and initialize // Advanced solutions based on Ocelot functionality config.AddOcelot(env, mergeTo, optional: false, reloadOnChange: true); // with environment and merging type config.AddOcelot(folder, env, mergeTo, optional: false, reloadOnChange: true); // with folder, environment and merging type config.AddOcelot(configuration, optional: false, reloadOnChange: true); // with configuration object created by your own config.AddOcelot(configuration, env, mergeTo, optional: false, reloadOnChange: true); // with configuration object, environment and merging type Examining the code within the ``ConfigurationBuilderExtensions`` class would be helpful for gaining a better understanding of the signatures of the overloaded :ref:`di-configuration-addocelot-methods`. .. _config-react-to-changes: React to Changes ---------------- Resolve ``IOcelotConfigurationChangeTokenSource`` interface from the DI container if you wish to react to changes to the Ocelot configuration via the :ref:`administration-api` or `ocelot.json`_ being reloaded from the disk. You may either poll the change token's ``IChangeToken.HasChanged`` property, or register a callback with the ``RegisterChangeCallback`` method. **How to poll** is explained here: .. code-block:: csharp public class ConfigurationNotifyingService : BackgroundService { private readonly IOcelotConfigurationChangeTokenSource _tokenSource; private readonly ILogger _logger; public ConfigurationNotifyingService(IOcelotConfigurationChangeTokenSource tokenSource, ILogger logger) { _tokenSource = tokenSource; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { if (_tokenSource.ChangeToken.HasChanged) { _logger.LogInformation("Configuration has changed"); } await Task.Delay(1000, stoppingToken); } } } **How to register a callback** is explained here: .. code-block:: csharp public sealed class MyConfigurationNotifying : IDisposable { private readonly IOcelotConfigurationChangeTokenSource _tokenSource; private readonly IDisposable _callbackHolder; public MyConfigurationNotifying(IOcelotConfigurationChangeTokenSource tokenSource) { _tokenSource = tokenSource; _callbackHolder = tokenSource.ChangeToken .RegisterChangeCallback(_ => Console.WriteLine("Configuration has changed"), null); } public void Dispose() => _callbackHolder.Dispose(); } Store in `Consul`_ ------------------ As a developer, if you have enabled :doc:`../features/servicediscovery` with `Consul`_ support in Ocelot, you may choose to manage your configuration saving to the *Consul* `KV store`_. Beyond the traditional methods of storing configuration in a file vs folder (:ref:`config-merging-files`), or in-memory (:ref:`config-merging-tomemory`), you also have the alternative to utilize the `Consul`_ server's storage capabilities. For further details on managing Ocelot configurations via a Consul instance, please consult the ":ref:`sd-consul-configuration-in-kv`" section. .. _config-build-from-scratch: Build From Scratch ------------------ Class: `FileConfiguration `_ Storing, reading, and writing static configurations may have limitations. Therefore, for more flexible and advanced scenarios the ``FileConfiguration`` object can be built from scratch in C# code of Ocelot application startup. Additionally after reading static configuration from various sources such as, remote file systems, remote storages or cloudages, you can rewrite options to the configuration. Ocelot does not provide a fluent syntax to build configuration on fly as other products do. However, it is possible to inject a ``FileConfiguration`` object during Ocelot startup using the :ref:`di-configuration-addocelot-methods` with a special parameter: .. code-block:: csharp public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, /* optional */); The method above will deserialize the object to disk. If you prefer to keep the configuration in memory, the following method includes the ``MergeOcelotJson`` parameter: .. code-block:: csharp public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, /* optional */); In summary, the final .NET 8+ solution should be written in `Program`_ using `top-level statements `_: .. code-block:: csharp :emphasize-lines: 8,13,14 using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Middleware; var builder = WebApplication.CreateBuilder(args); // Build Ocelot's configuration object on the fly: var config = new FileConfiguration(); // create new or read static state from anywhere // ... initialize or rewrite props: add routes, global config, etc. builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(config) // MergeOcelotJson.ToFile : writing config JSON back to disk .AddOcelot(config, builder.Environment, MergeOcelotJson.ToMemory); // merging to memory builder.Services .AddOcelot(builder.Configuration); var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); As a final step, you could add shutdown logic to save the complete configuration back to the storage, deserializing it to JSON format. .. _config-http-handler-options: ``HttpHandlerOptions`` ---------------------- | Class: `FileHttpHandlerOptions `_ | MS Learn: `SocketsHttpHandler Class `_ This route configuration section allows for following HTTP redirects, for instance, via the boolean ``AllowAutoRedirect`` option. These options can be set at the route or global level for both static and :ref:`dynamic routing `. Use ``HttpHandlerOptions`` in a route configuration to set up `HttpMessageHandler `_ behavior based on a `SocketsHttpHandler `_ instance: .. code-block:: json "HttpHandlerOptions": { "AllowAutoRedirect": false, "MaxConnectionsPerServer": 2147483647, // max integer "PooledConnectionLifetimeSeconds": 120, "UseCookieContainer": false, "UseProxy": false, "UseTracing": false } .. list-table:: :widths: 25 75 :header-rows: 1 * - *Option* - *Description* * - | ``AllowAutoRedirect`` | default: ``false`` - This value indicates whether the request should follow `Redirection messages `_ (HTTP 3xx status codes). Set it ``true`` if the request should automatically follow redirection responses from the downstream resource; otherwise ``false``. * - | ``MaxConnectionsPerServer`` | default: ``2147483647``, maximum integer - This controls how many connections the internal ``HttpMessageInvoker`` will open to a single :ref:`hosting-gotchas-iis`/:ref:`hosting-gotchas-kestrel` server. * - | ``PooledConnectionLifetimeSeconds`` | default: ``120`` seconds - This controls how long a connection can be in the pool to be considered reusable. Also refer to the **1st note** below! * - | ``UseCookieContainer`` | default: ``false`` - This indicates whether the handler uses the ``CookieContainer`` property to store server cookies and uses these cookies when sending requests. Also refer to the **2nd note** below! * - | ``UseProxy`` | default: ``false`` - Refer to MS Learn: `UseProxy Property `_ * - | ``UseTracing`` | default: ``false`` - This enables :doc:`../features/tracing` feature in Ocelot. Also refer to the **3rd note** below! .. note:: 1. If the ``PooledConnectionLifetimeSeconds`` option is not defined, the default value is ``120`` seconds, which is hardcoded in the `HttpHandlerOptions `_ class as the ``DefaultPooledConnectionLifetimeSeconds`` constant. 2. If you use the ``CookieContainer``, Ocelot caches the ``HttpMessageInvoker`` for each downstream service. This means that all requests to that downstream service will share the same cookies. Issue `274 `_ was created because a user noticed that the cookies were being shared. The Ocelot team tried to think of a nice way to handle this but we think it is impossible. If you don't cache the clients, that means each request gets a new client and therefore a new cookie container. If you clear the cookies from the cached client container, you get race conditions due to inflight requests. This would also mean that subsequent requests don't use the cookies from the previous response! All in all not a great situation. We would avoid setting ``UseCookieContainer`` to ``true`` unless you have a really really good reason. Just look at your response headers and forward the cookies back with your next request! 3. ``UseTracing`` option adds a tracing ``DelegatingHandler`` (aka ``Ocelot.Requester.ITracingHandler``) after obtaining it from ``ITracingHandlerFactory``, encapsulating the ``Ocelot.Logging.IOcelotTracer`` service of DI-container. 4. Prior to version `24.1`_, global ``HttpHandlerOptions`` were not accessible, as they were only available at the route level for static routes. Since version `24.1`_, global configuration is supported for both static and dynamic routes. .. _ssl-errors: SSL Errors ---------- If you want to ignore SSL warnings (errors), set the following in your route configuration: .. code-block:: json "DangerousAcceptAnyServerCertificateValidator": true **We don't recommend doing this!** The team suggests creating your own certificate and then getting it trusted by your local (or remote) machine, if you can. For ``https`` scheme, this fake validator was requested by issue `309 `_. For ``wss`` scheme, this fake validator was added by PR `1377 `_. **Note**: As a team, we do not consider it an ideal solution. On one hand, the community wants to have an option to work with self-signed certificates. But on the other hand, currently, source code scanners detect two serious security vulnerabilities because of this fake validator in version `20.0`_ and higher. The Ocelot team will rethink this unfortunate situation, and it is highly likely that this feature will at least be redesigned or removed completely. For now, the SSL fake validator makes sense in local development environments when a route has ``https`` or ``wss`` schemes with self-signed certificates for those routes. There are no other reasons to use the ``DangerousAcceptAnyServerCertificateValidator`` property at all! As a team, we highly recommend following these instructions when developing your gateway app with Ocelot: * **Local development environments**: Use this feature to avoid SSL errors for self-signed certificates in the case of ``https`` or ``wss`` schemes. We understand that some routes should have the downstream scheme exactly with SSL, because they are also in development and/or deployed using SSL protocols. However, we believe that, especially for local development, you can switch from ``https`` to ``http`` without any objection since the services are in development and there is no risk of data leakage. * **Remote development environments**: Everything is the same as for local development. However, this case is less strict; you have more options to use real certificates to switch off the feature. For instance, you can deploy downstream services to cloud and hosting providers that have their own signed certificates for SSL. At least your team can deploy one remote web server to host downstream services. Install your own certificate or use the cloud provider's one. * **Staging or testing environments**: We do not recommend using self-signed certificates because web servers should have valid certificates installed. Ask your system administrator or DevOps engineers to create valid certificates. * **Production environments**: **Do not use self-signed certificates at all!** System administrators or DevOps engineers must create real valid certificates signed by hosting or cloud providers. **Switch off the feature for all routes!** Remove the ``DangerousAcceptAnyServerCertificateValidator`` property for all routes in the production version of the `ocelot.json`_ file! .. _config-http-version: ``DownstreamHttpVersion`` ------------------------- MS Learn: `HttpVersion Class `_ Ocelot allows you to choose the HTTP version it will use to make the proxy request. It can be set as ``1.0``, ``1.1``, or ``2.0``. .. _config-version-policy: ``DownstreamHttpVersionPolicy`` [#f3]_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Enum: `HttpVersionPolicy `_ This routing property enables the configuration of the ``VersionPolicy`` property within ``HttpRequestMessage`` objects for downstream HTTP requests. For additional details, refer to the following documentation: * `HttpRequestMessage.VersionPolicy Property `_ * `HttpVersionPolicy Enum `_ * `HttpVersion Class `_ The ``DownstreamHttpVersionPolicy`` option is intricately linked with the :ref:`config-http-version` setting. Therefore, merely specifying ``DownstreamHttpVersion`` may sometimes be inadequate, particularly if your downstream services or Ocelot logs report HTTP connection errors such as ``PROTOCOL_ERROR``. In these routes, selecting the precise ``DownstreamHttpVersionPolicy`` value is crucial for the ``HttpVersion`` policy to prevent such protocol errors. HTTP2 version policy ^^^^^^^^^^^^^^^^^^^^ **Given** you aim to ensure a smooth HTTP/2 connection setup for the Ocelot app and downstream services with SSL enabled: .. code-block:: json { "DownstreamScheme": "https", "DownstreamHttpVersion": "2.0", "DownstreamHttpVersionPolicy": "", // empty or not defined "DangerousAcceptAnyServerCertificateValidator": true } **And** you configure global settings to use :ref:`hosting-gotchas-kestrel` with this snippet: .. code-block:: csharp var builder = WebApplication.CreateBuilder(args); builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = HttpProtocols.Http2; }); }); **When** all components are set to communicate exclusively via HTTP/2 without TLS (plain HTTP). **Then** the downstream services may display error messages such as: .. code-block:: HTTP/2 connection error (PROTOCOL_ERROR): Invalid HTTP/2 connection preface To resolve the issue, ensure that ``HttpRequestMessage`` has its ``VersionPolicy`` set to ``RequestVersionOrHigher``. Therefore, the ``DownstreamHttpVersionPolicy`` should be defined as follows: .. code-block:: json { "DownstreamHttpVersion": "2.0", "DownstreamHttpVersionPolicy": "RequestVersionOrHigher" // ! } Dependency Injection -------------------- Class: `ConfigurationBuilderExtensions`_ *Dependency Injection* for this *Configuration* feature in Ocelot is designed to extend and/or control the configuration of the Ocelot Core before the stage of building ASP.NET Core pipeline services. The primary methods are :ref:`di-configuration-addocelot-methods` within the ``ConfigurationBuilderExtensions`` class, which offers several overloaded versions with corresponding signatures. You can utilize these methods in the `Program`_.cs file of your gateway app to configure the Ocelot pipeline and services. Find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. .. _config-route-metadata: Extend with ``Metadata`` ------------------------ Feature: :doc:`../features/metadata` [#f4]_ The ``Metadata`` options can store any arbitrary data that users can access in middlewares, delegating handlers, etc. By using the *metadata*, users can implement their own logic and extend the functionality of Ocelot. The :doc:`../features/metadata` feature is designed to extend both the static :ref:`config-route-schema` and :ref:`config-dynamic-route-schema`. Global *metadata* must be defined in the ``Metadata`` section, while parsing options should be placed in the ``MetadataOptions`` section. The following example demonstrates practical usage of this feature: .. code-block:: json :emphasize-lines: 10,21-22 { "Routes": [ { // other opts... "Metadata": { "api-id": "FindPost", "my-extension/param1": "overwritten-value", "other-extension/param1": "value1", "other-extension/param2": "value2", "tags": "tag1, tag2, area1, area2, func1", "json": "[1, 2, 3, 4, 5]" } } ], "GlobalConfiguration": { // other opts... "Metadata": { "instance_name": "dc-1-54abcz", "my-extension/param1": "default-value" }, "MetadataOptions": { // parsing metadata opts... } } } .. _break3: http://break.do **Note**: Route *metadata* prevails over global *metadata* from the ``GlobalConfiguration`` section. Therefore, if the same key data are defined both at the route and global levels, the route *metadata* overrides the global ones. Now, the route *metadata* can be accessed through the `DownstreamRoute `_ object: .. code-block:: csharp :emphasize-lines: 8,9 using Ocelot.Metadata; public static class OcelotMiddlewares { public static Task PreAuthenticationMiddleware(HttpContext context, Func next) { var route = context.Items.DownstreamRoute(); var param1 = route.GetMetadata("my-extension/param1") ?? throw new ArgumentNullException("my-extension/param1"); var param2 = route.GetMetadata("other-extension/param2", "default-value"); // Working with metadata... return next(); } } For comprehensive documentation, please refer to the :doc:`../features/metadata` chapter. .. _config-timeout: ``Timeout`` ----------- This feature [#f5]_ is designed as part of the ``MessageInvokerPool``, which contains cached ``HttpMessageInvoker`` objects per route. Each created ``HttpMessageInvoker`` encapsulates an ``HttpMessageHandler``, specifically a ``SocketsHttpHandler`` instance, which serves as the base handler for the request pipeline. This pipeline also includes all user-defined :doc:`../features/delegatinghandlers`. Finally, both the :doc:`../features/delegatinghandlers` and the base ``SocketsHttpHandler`` are wrapped by Ocelot's custom ``TimeoutDelegatingHandler``, which provides the internal timeout functionality. **Note**: This design is subject to future review because ``TimeoutDelegatingHandler`` overrides/mimics the default timeout properties of ``SocketsHttpHandler``, as well as the behavior of ``HttpMessageInvoker`` as a controller for ``HttpMessageHandler`` objects. To configure timeouts (in seconds) at different levels, choose the appropriate level and provide the corresponding JSON configuration. Route timeout ^^^^^^^^^^^^^ A *route timeout* (also known as Requester middleware timeout based on ``TimeoutDelegatingHandler``) can be easily defined using the following JSON, according to the :ref:`config-route-schema`: .. code-block:: json { // upstream props // downstream props "Timeout": 3 // seconds } Please note that the route-level timeout takes precedence over the global timeout. The same configuration applies to *dynamic routes*, according to the :ref:`config-dynamic-route-schema`. Global timeout ^^^^^^^^^^^^^^ A *global configuration timeout* can be defined using the following JSON, according to the :ref:`config-global-configuration-schema`: .. code-block:: json { // routes... "GlobalConfiguration": { // other props "Timeout": 60 // seconds, 1 minute } } Please note that the global timeout is substituted into a route if the route-level timeout is not defined, and it takes precedence over the absolute :ref:`config-default-timeout`. Additionally, the global timeout may be omitted in the JSON configuration in favor of the absolute :ref:`config-default-timeout`, which is also configurable via a property of the C# static class. QoS timeout ^^^^^^^^^^^ A :doc:`../features/qualityofservice` (QoS) *timeout* can be defined using the :ref:`qos-schema` and the QoS :ref:`qos-timeout-strategy`: .. code-block:: json "QoSOptions": { "Timeout": 5000 // milliseconds } Please note, the *Quality of Service* timeout takes precedence over both route-level and global timeouts, which are ignored when QoS is enabled. Additionally, avoid defining both *timeouts* in the same route, as the QoS timeout has higher priority than the route-level timeout. Therefore, the following route configuration **is not** recommended: .. code-block:: json { // route props... "Timeout": 3, // seconds "QoSOptions": { "Timeout": 5000 // milliseconds } } So, route ``Timeout`` will be ignored in favor of QoS ``Timeout``. Moreover, because the 3-second duration is shorter than 5000 milliseconds, you may observe warning messages in the logs that begin with the following sentence: .. code-block:: text Route '/xxx' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: ... For more details about this warning, refer to the :ref:`qos-notes-qos-and-route-global-timeouts` note in the :doc:`../features/qualityofservice` chapter. Your next recommended action is to completely remove the 3-second ``Timeout`` property or comment it out: .. code-block:: json { // "Timeout": 3, // seconds "QoSOptions": { "Timeout": 5000 // milliseconds } } .. note:: 1. Both route ``Timeout`` and QoS ``Timeout`` are nullable positive integers, with a minimum valid value of ``1``. Values in the range ``(−∞, 0]`` are treated as "no value" and will be automatically converted to the absolute :ref:`config-default-timeout`, effectively ignoring the property. 2. The unit of measurement for route ``Timeout`` is seconds, whereas QoS ``Timeout`` is measured in milliseconds. .. _config-default-timeout: Default timeout ^^^^^^^^^^^^^^^ .. _DefTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22const+int+DefTimeout%22&type=code .. _DefaultTimeoutSeconds: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22static+int+DefaultTimeoutSeconds%22&type=code Timeout values defined at different levels in the JSON configuration can serve as fallback defaults for other levels. - The absolute timeout (also known as ``DownstreamRoute`` `DefaultTimeoutSeconds`_) defaults to 90 seconds (as defined by the ``DownstreamRoute`` `DefTimeout`_ constant). It acts as the default timeout when neither route-level nor global timeouts are defined. - The global configuration timeout, if not defined, also defaults to ``DownstreamRoute.DefaultTimeoutSeconds``. If defined, it serves as the default timeout for all routes. - The Quality of Service (QoS) global timeout acts as the default timeout for all routes where QoS is enabled. To configure the absolute timeout (currently 90 seconds, as defined by the ``DownstreamRoute`` `DefTimeout`_ constant), assign the desired number of seconds to the ``DownstreamRoute`` `DefaultTimeoutSeconds`_ static property in your `Program`_ class: .. code-block:: csharp using Ocelot.Configuration; DownstreamRoute.DefaultTimeoutSeconds = 3; // seconds, value must be >= 3 However, keep in mind that the absolute timeout has the lowest priority—therefore, route-level and global timeouts will override this C# property if they are defined. """" .. [#f1] The ":ref:`Merging Files `" feature was requested in issue `296`_, since then we extended it in issue `1216`_ (PR `1227`_) as ":ref:`Merging files to memory `" subfeature which was released as a part of version `23.2`_. .. [#f2] The ":ref:`Merging files to memory `" feature is based on the `MergeOcelotJson `_ enumeration type with values: ``ToFile`` and ``ToMemory``. The 1st one is implicit by default, and the second one is exactly what you need when merging to memory. See more details on implementations in the `ConfigurationBuilderExtensions`_ class. .. [#f3] The ":ref:`DownstreamHttpVersionPolicy `" feature was requested in issue `1672`_ as a part of version `23.3`_. .. [#f4] The ":ref:`config-route-metadata`" feature was requested in issues `738`_ and `1990`_, and it was released as part of version `23.3`_. .. [#f5] The initial draft design of the :ref:`config-timeout` feature was implemented in pull request `1824`_ as ``TimeoutDelegatingHandler`` (released in version `23.0`_), but this version supported only the built-in `default timeout of 90 seconds`_. The full :ref:`config-timeout` feature was requested in issue `1314`_, implemented in pull request `2073`_, and officially released as part of version `24.1`_. .. _default timeout of 90 seconds: https://github.com/ThreeMammals/Ocelot/blob/24.0.0/src/Ocelot/Requester/MessageInvokerPool.cs#L38 .. _296: https://github.com/ThreeMammals/Ocelot/issues/296 .. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. _738: https://github.com/ThreeMammals/Ocelot/issues/738 .. _1216: https://github.com/ThreeMammals/Ocelot/issues/1216 .. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227 .. _1314: https://github.com/ThreeMammals/Ocelot/issues/1314 .. _1672: https://github.com/ThreeMammals/Ocelot/issues/1672 .. _1824: https://github.com/ThreeMammals/Ocelot/pull/1824 .. _1990: https://github.com/ThreeMammals/Ocelot/issues/1990 .. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073 .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13 ================================================ FILE: docs/features/delegatinghandlers.rst ================================================ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs Delegating Handlers =================== **MS Learn Documentation:** * `DelegatingHandler Class `_ * `HTTP Message Handlers in ASP.NET Web API `_ * `HttpClient Message Handlers in ASP.NET Web API `_ Ocelot allows the user to add `delegating handlers `_ to the ``HttpClient`` transport. [#f1]_ Configuration ------------- In order to utilize the :doc:`../features/delegatinghandlers` feature, you need to do the following three steps of configuration. 1. Create a class that can be used as a *delegating handler*: it must inherit from the ``DelegatingHandler`` class. We are going to register these handlers in the ASP.NET Core DI container, so you can inject any other services you have registered into the constructor of your handler. .. code-block:: csharp public class MyHandler : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken token) { // Do stuff before sending request, and optionally call the base handler... var response = await base.SendAsync(request, token); // Do post-processing of the response... return response; } } 2. You must add the handlers to the DI container in your `Program`_, as shown below: .. code-block:: csharp builder.Services .AddOcelot(builder.Configuration) .AddDelegatingHandler() .AddDelegatingHandler(); Both of these ``AddDelegatingHandler{T}`` methods have an optional parameter called ``global``, which is set to ``false``. If it is ``false``, then the intent of the *delegating handler* is to be applied to specific routes via `ocelot.json`_ (see step 3). If it is set to ``true``, then it becomes a global handler and will be applied to all routes, as shown below: .. code-block:: csharp builder.Services .AddOcelot(builder.Configuration) .AddDelegatingHandler(true); // it's global! .. _break: http://break.do **Note 1**: The generic ``AddDelegatingHandler(bool)`` method has another overloaded non-generic one with the ``Type`` parameter: ``AddDelegatingHandler(Type, bool)``. Thus, here is an alternative to set it up: .. code-block:: csharp builder.Services .AddOcelot(builder.Configuration) .AddDelegatingHandler(typeof(MyHandler)) // for selected routes only .AddDelegatingHandler(typeof(MyGlobalHandler), true); // it's global! **Note 2**: Both versions of the methods add transient services to the DI container. It is recommended to utilize the generic version. 3. If you want route-specific *delegating handlers* or to order your specific and/or global *delegating handlers* (more on this in the :ref:`dh-execution-order` section), then you must add the following to the specific route in `ocelot.json`_. The names in the array must match the class names of your *delegating handlers* for Ocelot to match them together: .. code-block:: json "DelegatingHandlers": [ "MyHandlerTwo", "MyHandler" ] .. _dh-execution-order: Execution Order --------------- You can have as many *delegating handlers* as you want, and they are run in the following order: 1. Any globals that are left in the order they were added to services and are not in the ``DelegatingHandlers`` option array from `ocelot.json`_. 2. Any non-global *delegating handlers* plus any globals that were in the ``DelegatingHandlers`` option array from `ocelot.json`_, ordered as they are in the ``DelegatingHandlers`` array. 3. Tracing *delegating handler*, if enabled (refer to the :doc:`../features/tracing` chapter). 4. Quality of Service *delegating handler*, if enabled (refer to the :doc:`../features/qualityofservice` chapter). 5. The ``HttpClient`` sends the ``HttpRequestMessage``. Hopefully, other people will find this feature useful! """" .. [#f1] This feature was requested in issue `208`_, and the team decided that it would be useful in various ways, releasing it in version `3.0.3`_. Since then, we extended it in issue `264`_ and released it in version `5.0.0`_. .. _208: https://github.com/ThreeMammals/Ocelot/issues/208 .. _264: https://github.com/ThreeMammals/Ocelot/issues/264 .. _3.0.3: https://github.com/ThreeMammals/Ocelot/releases/tag/3.0.3 .. _5.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.0.0 ================================================ FILE: docs/features/dependencyinjection.rst ================================================ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs .. _ServiceCollectionExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs .. _ConfigurationBuilderExtensions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs Dependency Injection ==================== | Namespace: ``Ocelot.DependencyInjection`` | Source code: `DependencyInjection `_ .. _di-services-overview: Services Overview ----------------- *Dependency Injection* feature in Ocelot is designed to extend and/or control the building of Ocelot Core as ASP.NET Core pipeline services. The main methods of the `ServiceCollectionExtensions`_ class are: * The :ref:`di-services-addocelot-method` adds the required Ocelot services to the DI container and adds default services using the :ref:`di-adddefaultaspnetservices-method`. * The :ref:`di-addocelotusingbuilder-method` adds the required Ocelot services to the DI container and adds custom ASP.NET services with configuration injected implicitly or explicitly. Use :ref:`di-iservicecollection-extensions` in your `Program`_ (ASP.NET Core app) to add and build Ocelot Core services. The fact is, the :ref:`di-ocelotbuilder-class` is Ocelot's cornerstone logic. .. _di-iservicecollection-extensions: ``IServiceCollection`` extensions --------------------------------- Class: ``Ocelot.DependencyInjection.`` `ServiceCollectionExtensions`_ Based on the current implementations for the :ref:`di-ocelotbuilder-class`, the :ref:`di-services-addocelot-method` adds the required ASP.NET services to the DI container. You could call the more extended :ref:`di-addocelotusingbuilder-method` while configuring services to build and use a custom builder via an ``IMvcCoreBuilder`` object. .. _di-services-addocelot-method: ``AddOcelot`` method ^^^^^^^^^^^^^^^^^^^^ **Signatures**: .. code-block:: csharp IOcelotBuilder AddOcelot(this IServiceCollection services); IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration); These ``IServiceCollection`` extension methods add default ASP.NET services and Ocelot application services with configuration injected implicitly or explicitly. **Note**: Both methods add the required and *default* ASP.NET Core services for Ocelot Core in the :ref:`di-adddefaultaspnetservices-method`, which is the default builder. In this scenario, you do nothing other than call the ``AddOcelot`` method, which is often mentioned in feature chapters if additional startup settings are required. With this method, you simply reuse the default settings to build the Ocelot Core. The alternative is the ``AddOcelotUsingBuilder`` method; see the next subsection. .. _di-addocelotusingbuilder-method: ``AddOcelotUsingBuilder`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ **Signatures**: .. code-block:: csharp using CustomBuilderFunc = System.Func; IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, CustomBuilderFunc customBuilder); IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, CustomBuilderFunc customBuilder); These ``IServiceCollection`` extension methods add Ocelot application services and **custom** ASP.NET Core services with configuration injected implicitly or explicitly. **Note**: The method adds **custom** ASP.NET Core services required for Ocelot Core using a custom builder (aka ``customBuilder`` parameter). It is highly recommended to read the documentation of the :ref:`di-adddefaultaspnetservices-method`, or even review the implementation to understand the default ASP.NET Core services which are the minimal part of the gateway pipeline. In this custom scenario, you control everything during the ASP.NET Core build process, and you provide custom settings to build Ocelot Core. .. _di-ocelotbuilder-class: ``OcelotBuilder`` class ----------------------- The `OcelotBuilder `_ class is the core of Ocelot which does the following: - Contructs itself by single public constructor: .. code-block:: csharp public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null); - Initializes and stores public properties: ``Services`` (of ``IServiceCollection`` type), ``Configuration`` (of ``IConfiguration`` type), and ``MvcCoreBuilder`` (of ``IMvcCoreBuilder`` type). - Adds *all application services* during the construction phase via the ``Services`` property. - Adds ASP.NET Core services by builder using ``Func`` object in these 2 development scenarios: - Adds ASP.NET Core services by builder using a ``Func`` object in these two development scenarios: 1. By default builder (:ref:`di-adddefaultaspnetservices-method`) if there is no ``customBuilder`` parameter provided. 2. By :ref:`di-custom-builder` with the provided delegate object as the ``customBuilder`` parameter. - Adds (switches on/off) Ocelot features through the following methods: * ``AddSingletonDefinedAggregator`` and ``AddTransientDefinedAggregator`` methods * ``AddCustomLoadBalancer`` method * ``AddDelegatingHandler`` method * ``AddConfigPlaceholders`` method .. _di-adddefaultaspnetservices-method: ``AddDefaultAspNetServices`` method ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Part of the :ref:`di-ocelotbuilder-class` Currently, the method is protected, and overriding is forbidden. The role of the method is to inject the required services via both the ``IServiceCollection`` and ``IMvcCoreBuilder`` interface objects for the minimal part of the gateway pipeline. Current `implementation `_ is the folowing: .. code-block:: csharp protected IMvcCoreBuilder AddDefaultAspNetServices(IMvcCoreBuilder builder, Assembly assembly) { Services .AddLogging() .AddMiddlewareAnalysis() .AddWebEncoders(); return builder .AddApplicationPart(assembly) .AddControllersAsServices() .AddAuthorization() .AddNewtonsoftJson(); } The method cannot be overridden. It is not virtual, and there is no way to override the current behavior by inheritance. The method is the default builder of Ocelot Core when calling the :ref:`di-services-addocelot-method`. As an alternative, to "override" this default builder, you can design and reuse a custom builder as a ``Func`` delegate object and pass it as a parameter to the :ref:`di-addocelotusingbuilder-method`. It gives you full control over the design and building of Ocelot Core, but be careful when designing your custom Ocelot pipeline as a customizable ASP.NET Core pipeline. **Warning**: Most of the services from the minimal part of the pipeline should be reused, but only a few services can be removed. **Warning**: The method above is called after adding the required services of the ASP.NET Core pipeline by the `AddMvcCore `_ method via the ``Services`` property in the upper calling context. These services are the absolute minimum core services for the ASP.NET MVC pipeline. They must always be added to the DI container and are added implicitly before calling the method by the caller in the upper context. So, ``AddMvcCore`` creates an ``IMvcCoreBuilder`` object and assigns it to the ``MvcCoreBuilder`` property. Finally, as a default builder, the method above receives the ``IMvcCoreBuilder`` object, making it ready for further extensions. The next section shows you an example of designing a custom Ocelot Core using a custom builder. .. _di-custom-builder: Custom Builder -------------- **Goal**: Replace ``Newtonsoft.Json`` services with ``System.Text.Json`` services. Problem ^^^^^^^ The main :ref:`di-services-addocelot-method` adds `Newtonsoft JSON `_ services using the ``AddNewtonsoftJson`` extension method in the default builder (:ref:`di-adddefaultaspnetservices-method`). The ``AddNewtonsoftJson`` method was introduced in earlier .NET and Ocelot releases, which was necessary before Microsoft launched the ``System.Text.Json`` library. However, it now affects normal use, so we intend to solve the problem. Modern `JSON services `_ out of `the box `_ will help configure JSON settings using the ``JsonSerializerOptions`` property for JSON formatters during (de)serialization. Solution ^^^^^^^^ We have the following methods in `ServiceCollectionExtensions`_ class: .. code-block:: csharp IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder); IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder); These methods with a custom builder allow you to use any desired JSON library for (de)serialization. However, we are going to create a custom ``MvcCoreBuilder`` with support for JSON services, such as ``System.Text.Json``. To do that, we need to call the ``AddJsonOptions`` extension of the ``MvcCoreMvcCoreBuilderExtensions`` class (NuGet `Microsoft.AspNetCore.Mvc.Core `_ package) in `Program`_: .. code-block:: csharp :emphasize-lines: 6 builder.Services .AddLogging() .AddMiddlewareAnalysis() .AddWebEncoders() // Add your custom builder .AddOcelotUsingBuilder(builder.Configuration, MyCustomBuilder); static IMvcCoreBuilder MyCustomBuilder(IMvcCoreBuilder builder, Assembly assembly) => builder .AddApplicationPart(assembly) .AddControllersAsServices() .AddAuthorization() // Replace AddNewtonsoftJson() by AddJsonOptions() .AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; // use System.Text.Json }); The sample code provides settings to render JSON as indented text rather than as compressed plain JSON text without spaces. This is just one common use case, and you can add additional services to the builder. --------------------------------------------------------------------------------------------------------------------------- .. _di-configuration-overview: Configuration Overview ---------------------- *Dependency Injection* for the :doc:`../features/configuration` feature in Ocelot is designed to extend and set up the configuration of the Ocelot Core **before** the stage of building ASP.NET Core services (see :ref:`di-services-overview`). To configure the Ocelot Core services, use the :ref:`di-configuration-extensions` in your `Program`_ of your gateway app. .. _di-configuration-extensions: ``IConfigurationBuilder`` extensions ------------------------------------ Class: ``Ocelot.DependencyInjection.`` `ConfigurationBuilderExtensions`_ The main methods are the :ref:`di-configuration-addocelot-methods` within the `ConfigurationBuilderExtensions`_ class. These methods have a list of overloaded versions with corresponding signatures. The purpose of the ``AddOcelot`` method is to prepare everything before actually configuring with native extensions. It involves the following steps: 1. **Merging Partial JSON Files**: The ``GetMergedOcelotJson`` method merges partial JSON files. 2. **Selecting Merge Type**: It allows you to choose a merge type to save the merged JSON configuration data either ``ToFile`` or ``ToMemory``. 3. **Framework Extensions**: Finally, the method calls the following native ``IConfigurationBuilder`` framework extensions: * The ``AddJsonFile`` method adds the primary configuration file (commonly known as `ocelot.json`_) after the merge stage. It writes the file back *to the file system* using the ``ToFile`` merge type option, which is implicitly the default. * The ``AddJsonStream`` method adds the JSON data of the primary configuration file as a UTF-8 stream *into memory* after the merge stage. It uses the ``ToMemory`` merge type option. .. _di-configuration-addocelot-methods: ``AddOcelot`` methods ^^^^^^^^^^^^^^^^^^^^^ **Signatures** of the most common versions: .. code-block:: csharp IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env); IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env); .. _break: http://break.do **Note**: These versions use the implicit ``ToFile`` merge type to write `ocelot.json`_ back to disk. Finally, they call the ``AddJsonFile`` extension. **Signatures** of the versions to specify a ``MergeOcelotJson`` option: .. code-block:: csharp IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); .. _break2: http://break.do **Note**: These versions include optional arguments to specify the location of the three main files involved in the merge operation. In theory, these files can be located anywhere, but in practice, it is better to keep them in one folder. **Signatures** of the versions to indicate the ``FileConfiguration`` object of a self-created out-of-the-box configuration: [#f1]_ .. code-block:: csharp IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null); IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null); .. _break3: http://break.do **Note 1**: These versions include optional arguments to specify the location of the three main files involved in the merge operation. **Note 2**: Your ``FileConfiguration`` object can be serialized/deserialized from anywhere: local or remote storage, Consul KV storage, and even a database. For more information about this super useful feature, please read PR `1569`_. """" .. [#f1] The :ref:`config-build-from-scratch` feature was requested in issues `1228`_ and `1235`_. It was delivered by PR `1569`_ as part of version `20.0`_. Since then, we have extended it in PR `1227`_ and released it as part of version `23.2`_. .. _1227: https://github.com/ThreeMammals/Ocelot/pull/1227 .. _1228: https://github.com/ThreeMammals/Ocelot/issues/1228 .. _1235: https://github.com/ThreeMammals/Ocelot/issues/1235 .. _1569: https://github.com/ThreeMammals/Ocelot/pull/1569 .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 ================================================ FILE: docs/features/errorcodes.rst ================================================ Error Handling ============== .. _Handle errors in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling .. _standard error handling: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling MS Learn: `Handle errors in ASP.NET Core`_ Ocelot has custom error handling for ``Exception`` objects. Thus, we override the `standard error handling`_ provided by ASP.NET Core, which is based on manipulating ``Exception`` objects. .. _eh-middleware: Middleware ---------- .. _499 Client Closed Request: https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status499clientclosedrequest .. _500 Internal Server Error: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/500 Class: `ExceptionHandlerMiddleware `_ The ``ExceptionHandlerMiddleware`` produces the following status codes, in fallback order, after setting the :ref:`lg-request-id`: 1. Native response status: Returned when no exception is present, or when a mapped error status is available (excluding ``499`` and ``500``). 2. `499 Client Closed Request`_: A custom Ocelot status returned when an ``OperationCanceledException`` occurs due to an aborted request. A warning is logged. 3. `500 Internal Server Error`_: The standard status returned when a generic ``Exception`` occurs and Ocelot does not process or map the error. An error record is logged. Ocelot returns HTTP status codes based on internal logic in specific cases of :ref:`eh-client-error-responses` and :ref:`eh-server-error-responses`. .. _eh-client-error-responses: Client Error Responses ---------------------- .. _401 Unauthorized: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401 .. _403 Forbidden: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/403 .. _404 Not Found: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/404 .. _RequestCanceledError: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+RequestCanceledError&type=code .. _OcelotErrorCode.RequestCanceled: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20OcelotErrorCode.RequestCanceled&type=code - `401 Unauthorized`_: If the authentication middleware runs and the user is not authenticated. - `403 Forbidden`_: If the authorization middleware runs and the user is unauthorized, if the claim value is not authorized, if the scope is not authorized, if the user does not have the required claim, or if the claim cannot be found. - `404 Not Found`_: If a downstream route cannot be found, or if Ocelot is unable to map an internal error code to an HTTP status code. - `499 Client Closed Request`_: If the request is canceled by the client. | Ocelot Error: `RequestCanceledError `_ | Ocelot Code: `OcelotErrorCode.RequestCanceled `_ According to Ocelot Core's design, HTTP status code ``499`` is returned in the following ``OperationCanceledException`` scenarios: 1. By ``ExceptionHandlerMiddleware``, if an ``OperationCanceledException`` is thrown and the context's cancellation token is in the "cancellation requested" state. Ocelot logs a warning with the exception body. If the response has not started, the status code will be set to ``499``. 2. By ``ResponderMiddleware``, if the default ``IErrorsToHttpStatusCodeMapper`` service maps the detected `OcelotErrorCode.RequestCanceled`_ to status ``499``. This error code is produced by the ``IExceptionToErrorMapper`` service when an ``OperationCanceledException`` is thrown by other middlewares. .. _eh-server-error-responses: Server Error Responses ---------------------- .. _502 Bad Gateway: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/502 .. _503 Service Unavailable: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503 - `500 Internal Server Error`_: If unable to complete the HTTP request to the downstream service, and the exception is not ``OperationCanceledException`` or ``HttpRequestException``. - `502 Bad Gateway`_: If unable to connect to the downstream service. - `503 Service Unavailable`_: Returned when the downstream request times out. | Ocelot Error: `RequestTimedOutError `_ | Ocelot Code: `OcelotErrorCode.RequestTimedOutError `_ According to Ocelot Core's design, status code ``503`` is produced in the following ``TimeoutException`` scenarios: 1. By ``TimeoutDelegatingHandler`` from the ``IMessageInvokerPool`` service, when an ``OperationCanceledException`` is thrown and the context's cancellation token is not in the “cancellation requested” state. Ocelot does not log an error with the exception body, but the ``IExceptionToErrorMapper`` service generates the internal `OcelotErrorCode.RequestTimedOutError`_. 2. By ``ResponderMiddleware``, if the default ``IErrorsToHttpStatusCodeMapper`` service maps the detected `OcelotErrorCode.RequestTimedOutError`_ to status ``503``. This error code is produced by the ``IExceptionToErrorMapper`` service when a ``TimeoutException`` is thrown by other middlewares—especially by ``TimeoutDelegatingHandler``. .. _eh-error-mapper: Error Mapper ------------ Class: `HttpExceptionToErrorMapper `_ Historically, Ocelot errors are implemented by the `Exception-to-Error mapper `_. The ``Map`` method converts an ``Exception`` object to a native ``Ocelot.Errors.Error`` object. We override HTTP status codes because of ``Exception``-to-``Error`` mapping. This can be confusing for the developer since the actual status code of the downstream service may be different and get lost. Please research and review all response headers of the upstream service. If you do not find status codes and/or required headers, then the :doc:`../features/headerstransformation` feature should help. We expect you to share your use case with us in the `Discussions `_ space of the repository. |octocat| .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle ================================================ FILE: docs/features/graphql.rst ================================================ .. _GraphQL: https://graphql.org/ .. _Ocelot.Samples.GraphQL: https://github.com/ThreeMammals/Ocelot/tree/main/samples/GraphQL .. _graphql-dotnet: https://github.com/graphql-dotnet/graphql-dotnet .. |GraphQL Logo| image:: https://avatars.githubusercontent.com/u/13958777 :alt: GraphQL Logo :width: 40 |GraphQL Logo| GraphQL ====================== Ocelot does not directly support `GraphQL`_, but many people have asked about it. We wanted to show how easy it is to integrate the `GraphQL for .NET `_ library. Sample ------ **Sample**: `Ocelot.Samples.GraphQL`_ Please see the sample project `Ocelot.Samples.GraphQL`_. Using a combination of the `graphql-dotnet`_ project and Ocelot :doc:`../features/delegatinghandlers` feature, this is pretty easy to do. However, we do not intend to integrate more closely with `GraphQL`_ at the moment. Check out the sample's `README.md `_ for detailed instructions on how to do this. Future ------ If you have sufficient experience with `GraphQL`_ and the mentioned .NET `graphql-dotnet`_ package, we would welcome your contribution to the sample. |octocat| .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle Who knows, maybe you will get inspired by the sample development and come up with a design solution in the form of a rough draft of a *GraphQL* feature to implement in Ocelot. Good luck! And welcome to the `Discussions `_ space of the repository! ================================================ FILE: docs/features/headerstransformation.rst ================================================ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs Headers Transformation ====================== Ocelot allows the user to transform `HTTP headers `_ both before and after the downstream request. **Note**: *Headers Transformation* is generally available for static routes with a global configuration. For dynamic and aggregate routes, this feature is not implemented. This limitation is noted in the current :ref:`ht-roadmap`. Schema ------ As you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-schema` section, the route's *Headers Transformation* schema is quite simple, a JSON dictionary: .. code-block:: json "DownstreamHeaderTransform": { // "header_name": "transformation_expression", }, "UpstreamHeaderTransform": { // "header_name": "transformation_expression", }, Typically, a ``transformation_expression`` is a constant header value, a single placeholder from the :ref:`ht-placeholders` list, or a ":ref:`Find and Replace `" expression. Additionally, the :ref:`config-global-configuration-schema` allows configuring global *Headers Transformations* (refer to the :ref:`Configuration ` section). .. _ht-configuration: Configuration [#f1]_ -------------------- A complete *configuration* consists of both route-level and global *Headers Transformations*. .. code-block:: json { "Routes": [ { "DownstreamHeaderTransform": { // ... }, "UpstreamHeaderTransform": { // ... } } ], "GlobalConfiguration": { "DownstreamHeaderTransform": { // ... }, "UpstreamHeaderTransform": { // ... } } } .. _break: http://break.do .. _Merge: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22public+static+IEnumerable%3CHeader%3E+Merge%22&type=code **Note**: Route-level transformations take precedence over global transformations. In addition, when route-level transformations are defined, they do not entirely override the full set of header names from the global configuration. Ocelot's Core internal `Merge`_ algorithm identifies global header names not specified at the route level and appends them to the route's header set. .. _ht-find-and-replace: Find and Replace [#f2]_ ----------------------- In order to transform a header first we specify the header key and then the type of transform we want e.g. .. code-block:: json "Test": "http://www.bbc.co.uk/, http://ocelot.net/" The key is ``Test`` and the value is ``http://www.bbc.co.uk/, http://ocelot.net/``. The value is saying: replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``. The syntax is ``{find}, {replace}``. Hopefully pretty simple. There are examples below that explain more. **Pre Downstream Request** Add the following to a Route in `ocelot.json`_ in order to replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``. This header will be changed before the request downstream and will be sent to the downstream server. .. code-block:: json "UpstreamHeaderTransform": { "Test": "http://www.bbc.co.uk/, http://ocelot.net/" } **Post Downstream Request** Add the following to a Route in `ocelot.json`_ in order to replace ``http://www.bbc.co.uk/`` with ``http://ocelot.net/``. This transformation will take place after Ocelot has received the response from the downstream service. .. code-block:: json "DownstreamHeaderTransform": { "Test": "http://www.bbc.co.uk/, http://ocelot.net/" } .. _ht-add-to-request: Add to Request [#f3]_ --------------------- If you want to add a header to your upstream request please add the following to a route in your `ocelot.json`_: .. code-block:: json "UpstreamHeaderTransform": { "Uncle": "Bob" } In the example above a header with the key ``Uncle`` and value ``Bob`` would be send to to the upstream service. :ref:`ht-placeholders` are supported too (see below). .. _ht-add-to-response: Add to Response [#f4]_ ---------------------- If you want to add a header to your downstream response, please add the following to a route in `ocelot.json`_: .. code-block:: json "DownstreamHeaderTransform": { "Uncle": "Bob" } In the example above a header with the key ``Uncle`` and value ``Bob`` would be returned by Ocelot when requesting the specific route. If you want to return the :ref:`tr-butterfly` Trace ID, do something like the following: .. code-block:: json "DownstreamHeaderTransform": { "AnyKey": "{TraceId}" } .. _ht-placeholders: Placeholders ------------ Ocelot allows placeholders that can be used in header transformation. .. list-table:: :widths: 25 75 :header-rows: 1 * - *Placeholder* - *Description* * - ``{BaseUrl}`` - This will use Ocelot base URL e.g. ``http://localhost:5000`` as its value. * - ``{DownstreamBaseUrl}`` - This will use the downstream services base URL e.g. ``http://localhost:5000`` as its value. This only works for ``DownstreamHeaderTransform`` route option at the moment. * - ``{RemoteIpAddress}`` - This will find the clients IP address using ``HttpContext.Connection.RemoteIpAddress``, so you will get back some IP. See more in the `GetRemoteIpAddress `_ method. * - ``{TraceId}`` - This will use the :ref:`tr-butterfly` Trace ID. This only works for ``DownstreamHeaderTransform`` route option at the moment. * - ``{UpstreamHost}`` - This will look for the incoming ``Host`` header. For now, we believe these placeholders are sufficient for basic user scenarios. However, if you need additional placeholders, refer to the :ref:`ht-roadmap`. Samples ------- Handling 302 redirects ^^^^^^^^^^^^^^^^^^^^^^ Ocelot will by default automatically follow redirects, however if you want to return the location header to the client, you might want to change the location to be Ocelot not the downstream service. Ocelot allows this with the following configuration: .. code-block:: json "DownstreamHeaderTransform": { "Location": "http://www.bbc.co.uk/, http://ocelot.net/" }, "HttpHandlerOptions": { "AllowAutoRedirect": false, } Or, you could use the ``{BaseUrl}`` placeholder. .. code-block:: json "DownstreamHeaderTransform": { "Location": "http://localhost:6773, {BaseUrl}" }, "HttpHandlerOptions": { "AllowAutoRedirect": false, } Finally, if you are using a load balancer with Ocelot, you will get multiple downstream base URLs so the above would not work. In this case you can do the following: .. code-block:: json "DownstreamHeaderTransform": { "Location": "{DownstreamBaseUrl}, {BaseUrl}" }, "HttpHandlerOptions": { "AllowAutoRedirect": false, } ``X-Forwarded-For`` header ^^^^^^^^^^^^^^^^^^^^^^^^^^ An example of using ``{RemoteIpAddress}`` placeholder: .. code-block:: json "UpstreamHeaderTransform": { "X-Forwarded-For": "{RemoteIpAddress}" } .. _ht-roadmap: Roadmap ------- 1. Ideally the ":ref:`Find and Replace `" feature would be able to support the fact that a header can have multiple values. At the moment it just assumes one. It would also be nice if it could multi find and replace e.g. .. code-block:: json "DownstreamHeaderTransform": { "Location": "[{one,one},{two,two}]" }, "HttpHandlerOptions": { "AllowAutoRedirect": false, } .. _break2: http://break.do .. _moderate effort: https://github.com/ThreeMammals/Ocelot/labels/medium%20effort .. _significant effort: https://github.com/ThreeMammals/Ocelot/labels/large%20effort 2. The *Headers Transformation* feature is not implemented for :ref:`Dynamic Routes ` and :ref:`Aggregate Routes `. For :ref:`Dynamic Routing `, potential development would require `moderate effort`_. However, the Ocelot team expects that designing and implementing *Headers Transformation* for :doc:`../features/aggregation` will demand `significant effort`_, as aggregated routes typically lose their headers. .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle Ideas and proposals are welcome in the repository's `Discussions `_ space. |octocat| """" .. [#f1] The global :ref:`Configuration ` feature was requested in issue `1658`_ and released in version `24.1`_. .. [#f2] The ":ref:`Find and Replace `" feature was requested in issue `190`_, initially released in version `2.0.11`_, and the team decided that it would be useful in various ways. .. [#f3] The ":ref:`Add to Request `" feature was requested in issue `313`_ and released in version `5.5.3`_. .. [#f4] The ":ref:`Add to Response `" feature was requested in issue `280`_ and released in version `5.1.0`_. .. _2.0.11: https://github.com/ThreeMammals/Ocelot/releases/tag/2.0.11 .. _5.1.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.1.0 .. _5.5.3: https://github.com/ThreeMammals/Ocelot/releases/tag/5.5.3 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _190: https://github.com/ThreeMammals/Ocelot/issues/190 .. _280: https://github.com/ThreeMammals/Ocelot/issues/280 .. _313: https://github.com/ThreeMammals/Ocelot/issues/313 .. _1658: https://github.com/ThreeMammals/Ocelot/issues/1658 ================================================ FILE: docs/features/kubernetes.rst ================================================ .. role:: htm(raw) :format: html .. role:: pdf(raw) :format: latex pdflatex .. |K8sLogo| image:: https://raw.githubusercontent.com/kubernetes/kubernetes/master/logo/logo.png :alt: K8s Logo :height: 50 :class: img-valign-bottom :target: https://kubernetes.io .. |logo-kubernetes| image:: ../images/k8s-logo-kubernetes.png :alt: kubernetes logo :height: 30 :class: img-valign-middle :target: https://kubernetes.io .. _KubeClient: https://www.nuget.org/packages/KubeClient .. _Ocelot.Provider.Kubernetes: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes .. _package: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes |K8sLogo| Kubernetes (K8s) [#f1]_ ================================= | Feature of: :doc:`../features/servicediscovery` | Quick Links: `K8s Website `_ | `K8s Documentation `_ | `K8s GitHub `_ Ocelot will call the `K8s `_ endpoints API in a given namespace to get all of the endpoints for a pod and then load balance across them. Ocelot used to use the services API to send requests to the `K8s`_ service but this was changed in pull request `1134`_ because the service did not load balance as expected. Our NuGet `Ocelot.Provider.Kubernetes`_ extension package is based on the `KubeClient`_ package. For a comprehensive understanding, it is essential refer to the `KubeClient`_ documentation. .. _k8s-install: Install ------- The first thing you need to do is install the `package`_ that provides |logo-kubernetes| support in Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Kubernetes ``AddKubernetes(bool)`` method ------------------------------ .. code-block:: csharp :emphasize-lines: 3 public static class OcelotBuilderExtensions { public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true); } This extension-method adds `K8s`_ services **with** or **without** using a pod service account. Then add the following to your `Program `_: .. code-block:: csharp :emphasize-lines: 3 builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(); // usePodServiceAccount is true If you have services deployed in Kubernetes, you will normally use the naming service to access them. 1. By default the ``useServiceAccount`` argument is true, which means that Service Account using Pod to access the service of the `K8s`_ cluster needs to be Service Account based on RBAC authorization: You can replicate a Permissive using RBAC role bindings (see `Permissive RBAC Permissions `_), `K8s`_ API server and token will read from pod. .. code-block:: bash kubectl create clusterrolebinding permissive-binding --clusterrole=cluster-admin --user=admin --user=kubelet --group=system:serviceaccounts Finally, it creates the `KubeClient`_ from pod service account. 2. When the ``useServiceAccount`` argument is false, you need to provide `KubeClientOptions `_ to create `KubeClient`_ using them. You have to bind the options configuration section for the DI ``IOptions`` interface or register a custom action to initialize the options: .. code-block:: csharp :emphasize-lines: 9, 10, 13 Action configureKubeClient = opts => { opts.ApiEndPoint = new UriBuilder("https", "my-host", 443).Uri; opts.AccessToken = "my-token"; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AllowInsecure = true; }; builder.Services .AddOptions() .Configure(configureKubeClient); // manual binding options via IOptions builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(false); // don't use pod service account, and IOptions is reused .. _break: http://break.do **Note**, this could also be written like this (shortened version): .. code-block:: csharp :emphasize-lines: 2, 10 builder.Services .AddKubeClientOptions(opts => { opts.ApiEndPoint = new UriBuilder("https", "my-host", 443).Uri; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AccessToken = "my-token"; opts.AllowInsecure = true; }) .AddOcelot(builder.Configuration) .AddKubernetes(false); // don't use pod service account, and client options provided via AddKubeClientOptions Finally, it creates the `KubeClient`_ from your options. **Note 1**: For understanding the ``IOptions`` interface, please refer to the Microsoft Learn documentation: `Options pattern in .NET `_. **Note 2**: Please consider this Case 2 as an example of manual setup when you **do not** use a pod service account. We recommend using our official extension method, which receives an ``Action`` argument with your options: refer to the :ref:`k8s-addkubernetes-action-method` below. .. _k8s-addkubernetes-action-method: ``AddKubernetes(Action)`` method [#f2]_ ---------------------------------------------------------- .. code-block:: csharp :emphasize-lines: 3 public static class OcelotBuilderExtensions { public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, Action configureOptions, /*optional params*/); } This extension method adds `K8s`_ services **without** using a pod service account, explicitly calling an action to initialize configuration options for `KubeClient`_. It operates in two modes: 1. If ``configureOptions`` is provided (action is not null), it calls the action, ignoring all optional arguments. .. code-block:: csharp :emphasize-lines: 8 Action configureKubeClient = opts => { opts.ApiEndPoint = new UriBuilder("https", "my-host", 443).Uri; // ... }; builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(configureKubeClient); // without optional arguments .. _break: http://break.do **Note**: Optional arguments do not make sense; all settings are defined inside the ``configureKubeClient`` action. 2. If ``configureOptions`` is not provided (action is null), it reads the global ``ServiceDiscoveryProvider`` :ref:`k8s-configuration` options and reuses them to initialize the following properties: ``ApiEndPoint``, ``AccessToken``, and ``KubeNamespace``, finally initializing the rest of the properties with optional arguments. .. code-block:: csharp :emphasize-lines: 3, 5 builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(null, allowInsecure: true, /*optional args*/) // shortened version // or .AddKubernetes(configureOptions: null, allowInsecure: true, /*optional args*/); // long version .. _break2: http://break.do **Note**: Optional arguments must be used here in addition to the options coming from the global ``ServiceDiscoveryProvider`` :ref:`k8s-configuration`. Find the comprehensive documentation in the C# code of the `AddKubernetes `_ methods. .. _k8s-configuration: Configuration ------------- The following examples show how to set up a route that will work in Kubernetes. The most important thing is the ``ServiceName`` which is made up of the Kubernetes service name. We also need to set up the ``ServiceDiscoveryProvider`` in ``GlobalConfiguration``. Regarding global and route configurations, if your downstream service resides in a different namespace, you can override the global setting at the route level by specifying a ``ServiceNamespace``. .. code-block:: json "Routes": [ { "ServiceName": "my-service", "ServiceNamespace": "my-namespace" } ] .. _k8s-kube-provider: ``Kube`` provider ----------------- The example here shows a typical configuration: .. code-block:: json "Routes": [ { "ServiceName": "my-service", // ... } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Scheme": "https", "Host": "my-host", "Port": 443, "Token": "my-token", "Namespace": "Dev", "Type": "Kube" } } Service deployment in ``Dev`` namespace, and discovery provider type is ``Kube``, you also can set :ref:`PollKube ` or :ref:`WatchKube ` provider type. **Note 1**: ``Scheme``, ``Host``, ``Port``, and ``Token`` are not used if ``usePodServiceAccount`` is true when `KubeClient`_ is created from a pod service account. Please refer to the :ref:`k8s-install` section for technical details. **Note 2**: The ``Kube`` provider searches for the service entry using ``ServiceName`` and then retrieves the first available port from the ``EndpointSubsetV1.Ports`` collection. Therefore, if the port name is not specified, the default downstream scheme will be ``http``; Please refer to the ":ref:`Downstream Scheme vs Port Names `" section for technical details. .. _k8s-pollkube-provider: ``PollKube`` provider [#f3]_ ---------------------------- You use Ocelot to poll Kubernetes for latest service information rather than per request. If you want to poll Kubernetes for the latest services rather than per request (default behaviour of the :ref:`k8s-kube-provider`) then you need to set the following configuration: .. code-block:: json "ServiceDiscoveryProvider": { "Namespace": "dev", "Type": "PollKube", "PollingInterval": 100 // ms } The polling interval is in milliseconds and tells Ocelot how often to call Kubernetes for changes in service configuration. **Note**, there are tradeoffs here. If you poll Kubernetes, it is possible Ocelot will not know if a service is down depending on your polling interval and you might get more errors than if you get the latest services per request. This really depends on how volatile your services are. We doubt it will matter for most people and polling may give a tiny performance improvement over calling Kubernetes per request. There is no way for Ocelot to work these out for you, except perhaps through a `discussion `_. .. _k8s-watchkube-provider: ``WatchKube`` provider [#f4]_ ----------------------------- .. _Kubernetes API: https://kubernetes.io/docs/reference/using-api/ .. _watch requests: https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes With this configuration, `Kubernetes API`_ "`watch requests`_" are used to fetch service configuration. Essentially, it establishes one streamed HTTP connection with the `Kubernetes API`_ per downstream service. Changes streamed through this connection will be used to update the list of available endpoints. .. code-block:: json "ServiceDiscoveryProvider": { "Namespace": "dev", "Type": "WatchKube" } .. note:: The ``WatchKube`` provider is specifically designed for high-load Ocelot vs. Kubernetes environments with high RPS ratios. To better understand which type is suitable for your needs, we have added a table :ref:`k8s-comparing-providers`. The provider has an implicit configuration for fine-tuned watching, which are available and can only be initialized in C# code. * ``WatchKube.FirstResultsFetchingTimeoutSeconds``: `This `_ is the default number of seconds to wait after Ocelot starts, following the provider's creation, to fetch the first result from the Kubernetes endpoint. :sup:`1` * ``WatchKube.FailedSubscriptionRetrySeconds``: `This `__ is the default number of seconds to wait before scheduling the next retry for the subscription operation. :sup:`1` .. _break3: http://break.do :sup:`1` For both ``static int`` properties, the default value is 1 (one) second. The constraint ensures that the assigned value is greater than or equal to 1 (one). Therefore, the minimum value is 1 (one) second. .. _k8s-comparing-providers: Comparing providers ------------------- This table explains the most important indicators that may influence Ocelot vs. Kubernetes deployment or DevOps strategy. The evolution path of all providers follows: ``Kube`` -> ``PollKube`` -> ``WatchKube``, with ``WatchKube`` being the most advanced provider. .. list-table:: :widths: 34 22 22 22 :header-rows: 1 * - *Indicators \\ Providers* - :ref:`Kube ` - :ref:`PollKube ` - :ref:`WatchKube ` * - Extra latency - One hop per route - \- - \- * - Speed of response to endpoints changes - High - Low :sup:`1` - High * - Pressure on `Kubernetes API`_ - High - Low :sup:`1` - Low * - Ocelot load (estimated) :sup:`2` - < 1000 RPS - > 1000 RPS - > 5000 RPS * - Ocelot deployment :sup:`3` - Single instance - Multiple instances - Cluster of instances .. _break4: http://break.do | :sup:`1` Depends on the ``PollingInterval`` option. | :sup:`2` Please consider this a rough load estimation, as our team has not provided any tests or benchmarks. | :sup:`3` The term "instance" refers to an Ocelot instance, not a Kubernetes one. .. _k8s-downstream-scheme-vs-port-names: Downstream Scheme vs Port Names [#f5]_ -------------------------------------- Kubernetes configuration permits the definition of multiple ports with names for each address of an endpoint subset. When binding multiple ports, you assign a name to each subset port. To allow the ``Kube`` provider to recognize the desired port by its name, you need to specify the ``DownstreamScheme`` with the port's name; if not, the collection's first port entry will be chosen by default. For instance, consider a service on Kubernetes that exposes two ports: ``https`` for 443 and ``http`` for 80, as follows: .. code-block:: text Name: my-service Namespace: default Subsets: Addresses: 10.1.161.59 Ports: Name Port Protocol ---- ---- -------- https 443 TCP http 80 TCP **When** you need to use the ``http`` port while intentionally bypassing the default ``https`` port (first one), you must define ``DownstreamScheme`` to enable the provider to recognize the desired ``http`` port by comparing ``DownstreamScheme`` with the port name as follows: .. code-block:: json "Routes": [ { "ServiceName": "my-service", "DownstreamScheme": "http", // port name -> http -> port is 80 } ] .. note:: In the absence of a specified ``DownstreamScheme`` (the default behavior), the :ref:`k8s-kube-provider`—as well as other providers—will select *the first available port* from the ``EndpointSubsetV1.Ports`` collection. Consequently, if the port name is not designated, the default downstream scheme utilized will be ``http``. """" .. [#f1] The ":doc:`../features/kubernetes`" feature was requested as part of issue `345`_ to add support for `Kubernetes `_ :doc:`../features/servicediscovery` provider, and released in version `13.5.0`_ .. [#f2] The ":ref:`AddKubernetes(Action{KubeClientOptions}) method `" was requested as part of issue `2255`_ (pull request `2257`_), and released in version `24.0`_ .. [#f3] The evolution of the ":ref:`PollKube provider `" began with pull request `772`_ (version `13.2.0`_). Since then, the provider's design was reviewed due to reported bug `2304`_, and patch `2335`_ was applied and rolled out in version `24.1`_. .. [#f4] The ":ref:`WatchKube provider `" was first discussed in thread `2168`_, later implemented in pull request `2174`_, and released in version `24.1`_. .. [#f5] The ":ref:`Downstream Scheme vs Port Names `" feature was requested as part of issue `1967`_ and released in version `23.3`_ .. _345: https://github.com/ThreeMammals/Ocelot/issues/345 .. _772: https://github.com/ThreeMammals/Ocelot/pull/772 .. _1134: https://github.com/ThreeMammals/Ocelot/pull/1134 .. _1967: https://github.com/ThreeMammals/Ocelot/issues/1967 .. _2168: https://github.com/ThreeMammals/Ocelot/discussions/2168 .. _2174: https://github.com/ThreeMammals/Ocelot/pull/2174 .. _2255: https://github.com/ThreeMammals/Ocelot/issues/2255 .. _2257: https://github.com/ThreeMammals/Ocelot/pull/2257 .. _2304: https://github.com/ThreeMammals/Ocelot/issues/2304 .. _2335: https://github.com/ThreeMammals/Ocelot/pull/2335 .. _13.2.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.2.0 .. _13.5.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.0 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 ================================================ FILE: docs/features/loadbalancer.rst ================================================ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs Load Balancer ============= Ocelot can load balance across available downstream services for each route. This means you can scale your downstream services, and Ocelot can use them effectively. ``LoadBalancerOptions`` Schema ------------------------------ .. _FileLoadBalancerOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs Class: `FileLoadBalancerOptions`_ The following is the full *load balancer* configuration, used in both the :ref:`config-route-schema` and the :ref:`config-dynamic-route-schema`. Not all of these options need to be configured; however, the ``Type`` option is mandatory. .. code-block:: json "LoadBalancerOptions": { "Type": "", "Key": "", // CookieStickySessions balancer "Expiry": 1 // ms, CookieStickySessions balancer } .. list-table:: :widths: 15 85 :header-rows: 1 * - *Option* - *Description* * - ``Type`` - An in-built *load balancer* type selected from the list of available :ref:`lb-balancers`, or a user-defined type (refer to the ":ref:`Custom Balancers `" section). * - ``Key`` - The name of the cookie you wish to use for sticky sessions. This option is applicable only to the :ref:`CookieStickySessions type `. * - ``Expiry`` - Expiration period specifies how long, in milliseconds, the session should remain sticky. This value refreshes with each request to mimic typical session behavior. Note: This option applies only to the :ref:`CookieStickySessions type `. The actual ``LoadBalancerOptions`` schema with all the properties can be found in the C# `FileLoadBalancerOptions`_ class. .. _lb-configuration: Configuration ------------- The following shows how to set up multiple downstream services for a static route using `ocelot.json`_ and then select the ``LeastConnection`` *load balancer*. This is the simplest way to configure load balancing without using service discovery. .. code-block:: json :emphasize-lines: 10-12 { "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Put", "Delete" ], "DownstreamPathTemplate": "/api/posts/{postId}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "10.0.1.10", "Port": 5000 }, { "Host": "10.0.1.11", "Port": 5000 } ], "LoadBalancerOptions": { "Type": "LeastConnection" } } The following shows how to set up a route using :doc:`../features/servicediscovery` and then select the ``RoundRobin`` *load balancer*. .. code-block:: json { // ... "ServiceName": "product", "LoadBalancerOptions": { "Type": "RoundRobin" } } When this is set up, Ocelot will look up the downstream host and port from the :doc:`../features/servicediscovery` provider and load balance requests across any available services. If you add and remove services from the :doc:`../features/servicediscovery` provider [#f1]_, Ocelot should respect this and stop calling services that have been removed and start calling services that have been added. .. _lb-global-configuration: Global Configuration [#f2]_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ A complete configuration consists of both route-level and global *load balancing*. You can configure the following options in the ``GlobalConfiguration`` section of `ocelot.json`_: .. code-block:: json :emphasize-lines: 4-8, 12, 17-20 "Routes": [ { "Key": "R0", // optional "LoadBalancerOptions": { "Type": "CookieStickySessions", "Key": ".AspNetCore.Session", "Expiry": 1200000 // milliseconds, 20 minutes } }, { "Key": "R1", // this route is part of a group "LoadBalancerOptions": {} // optional due to grouping } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "LoadBalancerOptions": { "RouteKeys": ["R1"], // if undefined or empty array, opts will apply to all routes "Type": "LeastConnection" } } :doc:`../features/servicediscovery` dynamic routes intentionally override the global :ref:`dynamic routing ` configuration: .. code-block:: json :emphasize-lines: 5-7, 16-19 "DynamicRoutes": [ { "Key": "", // optional "ServiceName": "my-service", "LoadBalancerOptions": { "Type": "LeastConnection" // switch from RoundRobin to LeastConnection } } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "DownstreamScheme": "http", "ServiceDiscoveryProvider": { // required section for dynamic routing }, "LoadBalancerOptions": { "RouteKeys": [], // no grouping, thus opts apply to all dynamic routes "Type": "RoundRobin" } } In this configuration, the ``RoundRobin`` balancer is used for all implicit dynamic routes. However, for the "my-service" service, the load balancer type has been explicitly switched from ``RoundRobin`` to ``LeastConnection``. .. note:: 1. If the ``RouteKeys`` option is not defined or the array is empty in the global ``LoadBalancerOptions``, the global options will apply to all routes. If the array contains route keys, it defines a single group of routes to which the global options apply. Routes excluded from this group must specify their own route-level ``LoadBalancerOptions``. 2. Prior to version `24.1`_, global ``LoadBalancerOptions`` were only accessible in the special :ref:`Dynamic Routing ` mode. Since version `24.1`_, global configuration has been available for both static and dynamic routes. As a team, we would consider the idea of implementing such a global configuration for aggregated routes. However, an aggregated route is essentially a combination of static routes. .. _lb-balancers: Balancers --------- The available types of built-in *load balancers* are: .. list-table:: :widths: 25 75 :header-rows: 1 * - *Type* - *Description* * - ``CookieStickySessions`` - This uses a cookie to stick all requests to a specific server. More information can be found in the ":ref:`CookieStickySessions Type`" section. * - ``LeastConnection`` - This tracks which services are dealing with requests and sends new requests to the service with the fewest ("least") existing requests. The algorithm state is not distributed across a cluster of Ocelots. * - ``RoundRobin`` - This loops through available services and sends requests. The algorithm state is not distributed across a cluster of Ocelots. * - ``NoLoadBalancer`` - This takes the first available service from :ref:`configuration ` or :doc:`../features/servicediscovery` provider. You must choose which *load balancer* to use in your :ref:`configuration `. .. _lb-cookiestickysessions-type: ``CookieStickySessions`` Type [#f3]_ ------------------------------------ We have implemented a basic sticky session type of *load balancer*. The scenario it is meant to support involves having a number of downstream servers that do not share session state. If you receive more than one request for one of these servers, it should go to the same server each time; otherwise, the session state might be incorrect for the given user. In order to set up the ``CookieStickySessions`` *load balancer*, you need to do something like the following: .. code-block:: json { "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Put", "Delete" ], "DownstreamPathTemplate": "/api/posts/{postId}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "10.0.1.10", "Port": 5000 }, { "Host": "10.0.1.11", "Port": 5000 } ], "LoadBalancerOptions": { "Type": "CookieStickySessions", "Key": ".AspNetCore.Session", "Expiry": 1200000 // milliseconds, 20 minutes } } These ``LoadBalancerOptions`` configure the ``CookieStickySessions`` load balancer using the standard session cookie ``Key`` for ASP.NET Core apps with sessions enabled. The default expiration time is 20 minutes, matching the default session timeout in ASP.NET Core. **Note 1**: If you have multiple routes with the same ``LoadBalancerOptions``, then all of those routes will use the same *load balancer* for their subsequent requests. This means the sessions will be stuck across routes. **Note 2**: If you define more than one ``DownstreamHostAndPort``, or if you are using a :doc:`../features/servicediscovery` provider such as :ref:`sd-consul` and it returns more than one service, then ``CookieStickySessions`` uses ``RoundRobin`` to select the next server. This is hard-coded at the moment but could be changed. .. _lb-custom-balancers: Custom Balancers [#f4]_ ----------------------- In order to create and use a custom *load balancer*, you can do the following. Below, we set up a basic load balancing configuration, and note that the ``Type`` is ``MyLoadBalancer``, which is the name of a class we will set up to perform load balancing. .. code-block:: json { // ... "DownstreamHostAndPorts": [ { "Host": "10.0.1.10", "Port": 5000 }, { "Host": "10.0.1.11", "Port": 5000 } ], "LoadBalancerOptions": { "Type": "MyLoadBalancer" } } Then, you need to create a class that implements the ``ILoadBalancer`` interface. Below is a simple round-robin example: .. code-block:: csharp using Ocelot.LoadBalancer.LoadBalancers; using Ocelot.Responses; using Ocelot.Values; public class MyLoadBalancer : ILoadBalancer { private readonly Func>> _services; private static object Locker = new(); private int _last; public MyLoadBalancer() { } public MyLoadBalancer(Func>> services) => _services = services; public string Type => nameof(MyLoadBalancer); public void Release(ServiceHostAndPort hostAndPort) { } public async Task> LeaseAsync(HttpContext context) { var services = await _services.Invoke(); lock (Locker) { _last = (_last >= services.Count) ? 0 : _last; var next = services[_last++]; return new OkResponse(next.HostAndPort); } } } Finally, you need to register this class with Ocelot. We have used the most complex example below to show all of the data and types that can be passed into the factory that creates *load balancers*. .. code-block:: csharp using Ocelot.Configuration; using Ocelot.DependencyInjection; using Ocelot.ServiceDiscovery.Providers; Func lbFactory = (serviceProvider, Route, discoveryProvider) => new MyLoadBalancer(discoveryProvider.GetAsync); builder.Services .AddOcelot(builder.Configuration) .AddCustomLoadBalancer(lbFactory); However, there is a much simpler example that will work the same way: .. code-block:: csharp using Ocelot.DependencyInjection; builder.Services .AddOcelot(builder.Configuration) .AddCustomLoadBalancer(); .. note:: 1. There are numerous ``IOcelotBuilder`` `methods `_ to add a custom *load balancer*. The interface is as follows: .. code-block:: csharp IOcelotBuilder AddCustomLoadBalancer() where T : ILoadBalancer, new(); IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; 2. When you enable custom *load balancers*, Ocelot looks up your *load balancer* by its class name when it decides whether to perform load balancing. * If it finds a match, it will use your load balancer to load balance. * If Ocelot cannot match the *load balancer* type in your configuration with the name of the registered *load balancer* class, then you will receive an HTTP `500 Internal Server Error`_. * If your *load balancer* factory throws an exception when Ocelot calls it, you will receive an HTTP `500 Internal Server Error`_. .. warning:: Remember, if you specify no *load balancer* in your :ref:`lb-configuration`, Ocelot will not attempt to load balance. """" .. [#f1] Currently supported :doc:`../features/servicediscovery` providers are :ref:`sd-consul`, :doc:`Kubernetes <../features/kubernetes>`, :ref:`Eureka `, :doc:`../features/servicefabric`, and manually developed :ref:`sd-custom-providers`. .. [#f2] The ":ref:`Global Configuration `" feature, as part of issue `585`_, was introduced in pull request `2324`_ and released in version `24.1`_. .. [#f3] The ":ref:`CookieStickySessions Type `" feature was requested in issue `322`_, though what the user wants is more complicated than just sticky sessions. Anyway, we thought this would be a nice feature to have! Initially, the feature was released in version `6.0.0`_. .. [#f4] The ":ref:`Custom Balancers `" feature by `David Lievrouw`_ implemented a way to provide Ocelot with a custom *load balancer* in pull request `1155`_ (issue `961`_, released in version `15.0.3`_). .. _322: https://github.com/ThreeMammals/Ocelot/issues/322 .. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. _961: https://github.com/ThreeMammals/Ocelot/issues/961 .. _1155: https://github.com/ThreeMammals/Ocelot/pull/1155 .. _2324: https://github.com/ThreeMammals/Ocelot/pull/2324 .. _6.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/6.0.0 .. _15.0.3: https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.3 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _David Lievrouw: https://github.com/DavidLievrouw .. _500 Internal Server Error: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 ================================================ FILE: docs/features/logging.rst ================================================ .. _Logging in .NET Core and ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging .. _Serilog: https://serilog.net Logging ======= | MS Learn: `Logging in .NET Core and ASP.NET Core`_ | Interfaces: `ILoggerFactory `_ and `ILogger `_ Ocelot uses the standard ASP.NET Core logging interfaces ``ILoggerFactory`` and ``ILogger`` at the moment. This is encapsulated in `IOcelotLogger `_ and `IOcelotLoggerFactory `_ with the implementation for the standard `ASP.NET Core logging `_ stuff at the moment. This is because Ocelot adds some extra info to the logs such as :ref:`lg-request-id` if it is configured. There is a global :doc:`../features/errorcodes` :ref:`eh-middleware` that catches any exceptions thrown and logs them as errors. Finally, if logging is set to the ``Trace`` level, Ocelot will log the start, finish, and any middlewares that throw an exception, which can be quite useful. .. _lg-warning: Warning ------- If you are logging to the MS `Console `_, you will experience terrible performance. The community has encountered many performance issues with Ocelot, and it is always related to the ``Debug`` logging level when logging to the console/terminal. - **Warning**! Make sure you are logging to an appropriate destination/storage in the production environment! - Use ``Error`` and ``Critical`` levels in the production environment! - Use the ``Warning`` level in testing & staging environments! These and other recommendations can be found below in the :ref:`lg-best-practices` section. .. _lg-best-practices: Best Practices -------------- Microsoft Learn complete reference: `Logging in .NET Core and ASP.NET Core`_ Our recommendations for achieving the best logging with Ocelot are as follows. 1. Ensure the minimum log level while `Configure logging `_. The minimum log level is set in the application's ``appsettings.json`` file. This level is defined in the ``Logging`` section, for example: .. code-block:: json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } } } Whether you are using `Serilog`_ or the standard Microsoft providers, the logging configuration will be retrieved from this section. .. code-block:: csharp :emphasize-lines: 3 builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddJsonFile($"appsettings.{builder.Environment.EnvironmentName}.json", false, false) // read logging settings of the environment .AddOcelot(builder.Environment); However, there is one thing to be aware of. It is possible to use the ``SetMinimumLevel()`` method to define the minimum logging level. Be careful and make sure you set the log level in only one place, like this: .. code-block:: csharp builder.Logging .ClearProviders() .SetMinimumLevel(LogLevel.Warning); // MS Console for Development and/or Testing environments only if (!builder.Environment.IsProduction()) { builder.Logging.AddConsole(); } Please also use the ``ClearProviders()`` method so that only the providers you wish to use are taken into account, such as the console in the example above. 2. Ensure the proper usage of the minimum logging level for each environment: development, testing, production, etc. So, once again, read the important notes in the :ref:`lg-warning` section! 3. Ocelot's logging has been improved in version `22.0`_: it is now possible to use a factory method for message strings that will only be executed if the minimum log level allows it. For example, let's take a message containing information about several variables that should only be generated if the minimum log level is ``Debug``. If the minimum log level is ``Warning``, then the string is never generated. Therefore, when the string contains dynamic information (e.g., ``string.Format``), or the string value is generated by a `string interpolation `_ expression, it is recommended to call the ``LogX`` method using an anonymous delegate via an ``=>`` expression function: .. code-block:: csharp Logger.LogDebug( () => $"Downstream template is {httpContext.Items.DownstreamRoute().DownstreamPathTemplate.Value}"); otherwise a constant string is sufficient .. code-block:: csharp Logger.LogDebug("My const string"); .. _lg-request-id: Request ID ---------- Also known as "Correlation ID" or `HttpContext.TraceIdentifier `_ Ocelot allows a client to send a *Request ID* through an HTTP header. If provided, Ocelot uses the *Request ID* for logging as soon as it becomes available in the middleware pipeline. Additionally, Ocelot forwards the *Request ID* via the specified header to the downstream service. * You can still obtain the ASP.NET Core *Request ID* in the logs if you set ``IncludeScopes`` to ``true`` in your logging configuration. * The reason for not just using the `bog standard `_ framework logging is that we could not work out how to override the ``RequestId`` that gets logged when setting ``IncludeScopes`` to ``true`` in the logging settings. Nicely onto the next feature. Every log record has these 2 properties: .. list-table:: :widths: 25 75 :header-rows: 1 * - *Property* - *Description* * - ``RequestId`` - This represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001`` * - ``PreviousRequestId`` - This represents ID of the previous request .. _break: http://break.do As an ``IOcelotLogger`` interface object is injected into the constructors of service classes, the current default Ocelot logger (``OcelotLogger`` class) reads these two properties from the ``IRequestScopedDataRepository`` interface object. Find out more about these properties and other details on the *Request ID* logging feature below. Configuration ^^^^^^^^^^^^^ In order to use the *Request ID* feature, you have two options: specifying it globally or for the route. In your `ocelot.json`_, set the following configuration in the ``GlobalConfiguration`` section. This setting will apply to all requests processed by Ocelot. .. code-block:: json "GlobalConfiguration": { "RequestIdKey": "Oc-RequestId" } .. _break: http://break.do We recommend using the ``GlobalConfiguration`` unless it is absolutely necessary to make it route-specific. If you want to override this for a specific route, add the following to `ocelot.json`_: .. code-block:: json "RequestIdKey": "Oc-RequestId" Once Ocelot identifies incoming requests that match a route, it will set the *Request ID* based on the route configuration. Problem ^^^^^^^ This can lead to a small issue. If you set a ``GlobalConfiguration``, it is possible to use one *Request ID* until the route is identified and then another afterward, as the *Request ID* key can change. This behavior is intentional and represents the best solution we have devised for now. In this case, the ``OcelotLogger`` will display both the current *Request ID* and the previous *Request ID* in the logs. Below is an example of the logging when the ``Debug`` level is set for a normal request: .. code-block:: text info: Microsoft.AspNetCore.Hosting.Diagnostics[1] Request starting HTTP/1.1 GET https://localhost:7778/ocelot2/posts/3 - - - dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Ocelot pipeline started dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Upstream URL path: /ocelot2/posts/3 dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Downstream templates: /ocelot/posts/{id} info: Ocelot.RateLimiting.Middleware.RateLimitingMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - EnableEndpointEndpointRateLimiting is not enabled for downstream path: /ocelot/posts/{id} info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - No authentication needed for path: /ocelot2/posts/3 info: Ocelot.Authorization.Middleware.AuthorizationMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - No authorization needed for upstream path: /ocelot2/posts/{id} dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Downstream URL: http://localhost:5555/ocelot/posts/3 info: Microsoft.AspNetCore.Hosting.Diagnostics[1] Request starting HTTP/1.1 GET https://localhost:7778/ocelot2/posts/5 - - - dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Ocelot pipeline started dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Upstream URL path: /ocelot2/posts/5 dbug: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Downstream templates: /ocelot/posts/{id} info: Ocelot.RateLimiting.Middleware.RateLimitingMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 EnableEndpointEndpointRateLimiting is not enabled for downstream path: /ocelot/posts/{id} info: Ocelot.Authentication.Middleware.AuthenticationMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 No authentication needed for path: /ocelot2/posts/5 info: Ocelot.Authorization.Middleware.AuthorizationMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 No authorization needed for upstream path: /ocelot2/posts/{id} dbug: Ocelot.DownstreamUrlCreator.Middleware.DownstreamUrlCreatorMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Downstream URL: http://localhost:5555/ocelot/posts/5 info: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - 200 OK status code of request URI: http://localhost:5555/ocelot/posts/3 dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Setting HTTP response message... dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - No pipeline errors: setting and returning completed response... dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] RequestId: 0HNBA3NEIQUNJ:11111111, PreviousRequestId: - Ocelot pipeline finished info: Microsoft.AspNetCore.Hosting.Diagnostics[2] Request finished HTTP/1.1 GET https://localhost:7778/ocelot2/posts/3 - 200 84 application/json;+charset=utf-8 404.7256ms info: Microsoft.AspNetCore.Hosting.Diagnostics[16] Request reached the end of the middleware pipeline without being handled by application code. Request path: GET https://localhost:7778/ocelot2/posts/3, Response status code: 200 info: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 200 OK status code of request URI: http://localhost:5555/ocelot/posts/5 dbug: Ocelot.Requester.Middleware.HttpRequesterMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Setting HTTP response message... dbug: Ocelot.Responder.Middleware.ResponderMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 No pipeline errors: setting and returning completed response... dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] RequestId: 0HNBA3NEIQUNK:AAAAAAAA, PreviousRequestId: 0HNBA3NEIQUNJ:11111111 Ocelot pipeline finished info: Microsoft.AspNetCore.Hosting.Diagnostics[2] Request finished HTTP/1.1 GET https://localhost:7778/ocelot2/posts/5 - 200 128 application/json;+charset=utf-8 347.2607ms info: Microsoft.AspNetCore.Hosting.Diagnostics[16] Request reached the end of the middleware pipeline without being handled by application code. Request path: GET https://localhost:7778/ocelot2/posts/5, Response status code: 200 .. Note by Maintainer: .. The PreviousRequestId feature requires review and possible redesign, as it may not be implemented or could be broken. .. Typically, PreviousRequestId is '-' for all requests. Technical Facts ^^^^^^^^^^^^^^^ * Every log record has these 2 properties: * ``RequestId`` represents ID of the current request as plain string, for example ``0HNBA3NEIQUNJ:00000001``. * ``PreviousRequestId`` represents ID of the previous request. * As an ``IOcelotLogger`` interface object is injected into the constructors of service classes, the current default Ocelot logger (the ``OcelotLogger`` class) retrieves these two properties from the ``IRequestScopedDataRepository`` service. .. _lg-performance: Performance [#f1]_ ------------------ Here is a quick recipe for your production environment to achieve top *performance*. You need to ensure the minimum log level is ``Critical`` or ``None``. Nothing more! Having top logging *performance* means having fewer log records written by the logging provider. So, the logs should be pretty empty. Anyway, during the initial period after a version release to production, we recommend monitoring the system and the current version's app behavior by specifying the minimum log level as ``Error``. If the release engineer ensures the stability of the version in production, then the minimum log level can be increased to ``Critical`` or ``None`` to achieve top *performance*. Technically, this will disable the logging feature entirely. Benchmarks ---------- We currently have two types of benchmarks: - ``SerilogBenchmarks`` with `Serilog`_ logging to a file. See the ``ConfigureLogging`` method with ``logging.AddSerilog(_logger)``. - ``MsLoggerBenchmarks`` with MS default logging to the MS Console. See the ``ConfigureLogging`` method with ``logging.AddConsole()``. Benchmark results largely depend on the environment and hardware on which they run. We are pleased to invite you to run logging benchmarks on your machine by following the instructions below. 1. Open PowerShell or Command Prompt console 2. Build the Ocelot solution in Release mode: ``dotnet build --configuration Release`` 3. Go to the ``test\Ocelot.Benchmarks\bin\Release\`` folder 4. Choose the .NET version by changing the folder, for example, to ``net9.0`` 5. Run benchmarks: ``.\Ocelot.Benchmarks.exe`` 6. Run ``SerilogBenchmarks`` or ``MsLoggerBenchmarks`` by pressing the appropriate number of a benchmark (5 or 6), then press Enter. 7. Wait for 3+ minutes to complete the benchmark and get the final results. 8. Read and analyze your benchmark session results. .. Indicators ^^^^^^^^^^ To be developed... """" .. [#f1] Logging :ref:`performance ` was improved in pull request `1745`_ and released in version `22.0`_. These changes were requested as part of issue `1744`_ following the team's discussion in thread `1736`_. .. _22.0: https://github.com/ThreeMammals/Ocelot/releases/tag/22.0.0 .. _1745: https://github.com/ThreeMammals/Ocelot/pull/1745 .. _1744: https://github.com/ThreeMammals/Ocelot/issues/1744 .. _1736: https://github.com/ThreeMammals/Ocelot/discussions/1736 .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json ================================================ FILE: docs/features/metadata.rst ================================================ Metadata ======== [#f1]_ Feature of: :doc:`../features/configuration` Ocelot provides various features such as routing, authentication, caching, load balancing, and more. However, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior. In such cases, Ocelot allows users to add *metadata* to the route configuration. This property can store any arbitrary data that users can access in middlewares or delegating handlers. Schema ------ As you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-metadata` section, the route *metadata* schema is quite simple which is JSON dictionary: .. code-block:: json "Metadata": { // "key": "value", } .. _FileMetadataOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileMetadataOptions.cs However, **global** metadata configuration consists of both the ``Metadata`` and ``MetadataOptions`` sections. You do not need to set all of these things, but this is everything that is available at the moment. .. code-block:: json "GlobalConfiguration": { "Metadata": { // "key": "value", }, "MetadataOptions": { "CurrentCulture": "en-GB", "NumberStyle": "Any", "Separators": [","], "StringSplitOption": "None", "TrimChars": [" "], } } The actual global *metadata* schema with all the properties can be found in the C# `FileMetadataOptions`_ class. This configuration type is parsed to a `MetadataOptions `_ type object. .. list-table:: :widths: 20 80 :header-rows: 1 * - *Option* - *Description* * - ``CurrentCulture`` - | Parsed as the ``System.Globalization.CultureInfo`` object (refer to `CultureInfo `_ class) | Default value is current culture aka ``CultureInfo.CurrentCulture.Name`` * - ``NumberStyle`` - | Parsed as the ``System.Globalization.NumberStyles`` object (refer to `NumberStyles `_ enum) | Default value is ``NumberStyles.Any`` * - ``Separators`` - Array of ``string``. Default value is ``[","]`` aka comma. * - ``StringSplitOption`` - | Parsed as the ``System.StringSplitOptions`` object (refer to `StringSplitOptions `_ enum) | Default value is ``StringSplitOptions.None`` * - ``TrimChars`` - Array of ``char``. Default value is ``[" "]`` aka whitespace. * - ``Metadata`` - | Parsed as the ``Dictionary`` object containing all global *metadata* which ``string`` values are parsed to a target type value by the :ref:`md-getmetadata-method`. Configuration ------------- By using the *metadata*, users can implement their own logic and extend the functionality of Ocelot e.g. .. code-block:: json { "Routes": [ { "UpstreamHttpMethod": [ "GET" ], "UpstreamPathTemplate": "/posts/{postId}", "DownstreamPathTemplate": "/api/posts/{postId}", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 80 } ], "Metadata": { "id": "FindPost", "tags": "tag1, tag2, area1, area2, func1", "plugin1.enabled": "true", "plugin1.values": "[1, 2, 3, 4, 5]", "plugin1.param": "value2", "plugin1.param2": "123", "plugin2/param1": "overwritten-value", "plugin2/data": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}" } } ], "GlobalConfiguration": { "Metadata": { "instance_name": "machine-1", "plugin2/param1": "default-value" }, "MetadataOptions": { } } } Now, the route *metadata* can be accessed through the ``DownstreamRoute`` object: .. code-block:: csharp :emphasize-lines: 20 using Ocelot.Middleware; using Ocelot.Metadata; using Ocelot.Logging; public class MyMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IMyService _myService; public MyMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IMyService myService) : base(loggerFactory.CreateLogger()) { _next = next; _myService = myService; } public Task Invoke(HttpContext context) { Logger.LogDebug("My middleware started"); var route = context.Items.DownstreamRoute(); var id = route.GetMetadata("id"); var tags = route.GetMetadata("tags"); // Plugin 1 data var p1Enabled = route.GetMetadata("plugin1.enabled"); var p1Values = route.GetMetadata("plugin1.values"); var p1Param = route.GetMetadata("plugin1.param", "system-default-value"); var p1Param2 = route.GetMetadata("plugin1.param2"); // Plugin 2 data var p2Param1 = route.GetMetadata("plugin2/param1", "default-value"); var json = route.GetMetadata("plugin2/data"); var plugin2 = System.Text.Json.JsonSerializer.Deserialize(json); // Reading global metadata var globalInstanceName = route.GetMetadata("instance_name"); var globalPlugin2Param1 = route.GetMetadata("plugin2/param1"); // Working with plugin's metadata // ... return _next.Invoke(context); } public class Plugin2Data { public string name { get; set; } public int age { get; set; } public string city { get; set; } public bool is_student { get; set; } public string[] hobbies { get; set; } } } .. _md-getmetadata-method: ``GetMetadata`` Method ------------------------- Ocelot provides one ``DowstreamRoute`` extension method to help you retrieve your *metadata* values effortlessly. With the exception of the types ``string``, ``bool``, ``bool?``, ``string[]`` and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T. If the value is null, then, if not explicitely specified, the default for the chosen target type is returned. .. list-table:: :widths: 20 80 :header-rows: 1 * - *Method* - *Description* * - ``GetMetadata`` - The *metadata* value is returned as string without further parsing * - ``GetMetadata`` - | The *metadata* value is splitted by a given separator (default ``,``) and returned as a string array. | **Note**: Several parameters can be set in the global configuration, such as ``Separators`` (default = ``[","]``), ``StringSplitOptions`` (default ``None``) and ``TrimChars``, the characters that should be trimmed (default = ``[' ']``). * - ``GetMetadata`` - | The *metadata* value is parsed to a number. The ``TInt`` is any known numeric type, such as ``byte``, ``sbyte``, ``short``, ``ushort``, ``int``, ``uint``, ``long``, ``ulong``, ``float``, ``double``, ``decimal``. | **Note**: Some parameters can be set in the global configuration, such as ``NumberStyle`` (default ``Any``) and ``CurrentCulture`` (default ``CultureInfo.CurrentCulture``) * - ``GetMetadata`` - | The *metadata* value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type. | **Note**: A ``JsonSerializerOptions`` object can be passed as method parameter, ``Web`` is used as default. * - ``GetMetadata`` - | Check if the *metadata* value is a truthy value, otherwise return ``false``. | **Note**: The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled`` * - ``GetMetadata`` - | Check if the *metadata* value is a truthy value (return ``true``), or falsy value (return ``false``), otherwise return ``null``. | **Note**: The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0`` Sample ------ The *Metadata* feature is a relatively new :doc:`../features/configuration` feature (anchored in the ":ref:`config-route-metadata`" section). To introduce a standardized approach to middleware development, we have prepared a comprehensive sample project: | **Project**: `samples `_ / `Metadata `_ | **Solution**: `Ocelot.Samples.sln `_ The solution for the ``Ocelot.Samples.Metadata.csproj`` project includes the following capabilities: - It has two custom Ocelot middlewares attached: ``PreErrorResponderMiddleware`` and ``ResponderMiddleware``. The ``PreErrorResponderMiddleware`` reads the route *metadata* based on the route ID and parses it. This is an example of how to parse or read the *metadata* of a specific route. - The custom ``ResponderMiddleware`` simply calls the base Ocelot middleware (default implementation). Ocelot's ``ResponderMiddleware`` is responsible for writing the final body data into the ``HttpResponse`` of the current ``HttpContext``. - The main `Program`_ replaces Ocelot's default ``IHttpResponder`` service with a custom ``MetadataResponder`` service. It attaches both ``PreErrorResponderMiddleware`` and ``ResponderMiddleware`` using the ``OcelotPipelineConfiguration`` argument in the ``UseOcelot`` method. - The ``MetadataResponder`` service processes all JSON data when the ``Content-Type`` header has the value ``application/json``. This custom responder service writes the original data into the ``Response`` section and writes the route *metadata* back to the ``Metadata`` section using the following JSON schema: .. code-block:: json { "Response": { // Original data of the downstream response }, "Metadata": { // current route metadata } } - The ``MetadataResponder`` service always generates the custom ``OC-Route-Metadata`` header, containing the route *metadata* as a plain JSON string for all routes, regardless of the media type of the content. This allows you to parse it on the client side for specific purposes. - The ``MetadataResponder`` service attempts to decompress the content body if it is compressed using one of the following algorithms from downstream endpoints: Brotli (``br``), GZip (``gzip``), or Zstandard (``zstd``). However, data compressed with the ``deflate`` algorithm is ignored and transferred to the client as-is because decompressing a third-party algorithm with a custom implementation is not feasible. Finally, the responder service returns uncompressed data and indicates this in the ``Content-Encoding`` header, where the value is always set to ``identity``. - Processing JSON data can be disabled for specific routes using the ``disableMetadataJson`` option in the *metadata*. In this case, all JSON data is returned to the client as-is, preserving the original body streams (see the ``/ocelot/docs/`` route). **Conclusion**: The purpose of this sample is to detect JSON data, process it, and embed a custom ``Metadata`` section while returning the original JSON data in the ``Response`` section. This sample and its ``MetadataResponder`` service significantly increase response time due to on-the-fly JSON data processing, leading to degraded overall performance. Please consider this as an example of processing *metadata*. For production environments, such processing should be disabled. Instead, returning *metadata* in a custom header is likely the best solution if your client needs to know the currently executed route on Ocelot's side. """" .. [#f1] The *Metadata* feature was requested in issues `738`_ and `1990`_, and it was released as part of version `23.3`_. .. _738: https://github.com/ThreeMammals/Ocelot/issues/738 .. _1990: https://github.com/ThreeMammals/Ocelot/issues/1990 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Metadata/Program.cs ================================================ FILE: docs/features/methodtransformation.rst ================================================ Method Transformation [#f1]_ ============================ Ocelot allows users to modify the HTTP request method used when making requests to a downstream service. This is achieved by setting the following route configuration: .. code-block:: json { "UpstreamPathTemplate": "/{everything}", "DownstreamPathTemplate": "/{everything}", // other props and opts... "UpstreamHttpMethod": [ "Get" ], // we transform HTTP verb... "DownstreamHttpMethod": "Post" // ...from GET to POST } The key property here is ``DownstreamHttpMethod``, which is set to ``POST``, and the route will only match ``GET``, as specified by ``UpstreamHttpMethod``. This feature is useful when interacting with downstream APIs that only support ``POST`` while presenting a RESTful interface. """" .. [#f1] The *"Method Transformation"* feature was released in version `14.0.8`_. .. _14.0.8: https://github.com/ThreeMammals/Ocelot/releases/tag/14.0.8 ================================================ FILE: docs/features/middlewareinjection.rst ================================================ .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Metadata/Program.cs Middleware Injection ==================== When setting up Ocelot in your `Program`_, you can provide additional middleware and override it with your custom middlewares. This is done as follows: .. code-block:: csharp // Set it up: configuration, services, etc. // Middleware setup is only possible during the final stage of app configuration and execution var app = builder.Build(); var pipeline = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (context, next) => { await next.Invoke(); } }; await app.UseOcelot(pipeline); await app.RunAsync(); In the example above, the provided function will run before the first piece of Ocelot middleware. This allows users to supply any behavior they want before and after the Ocelot pipeline has run. .. warning:: Be cautious, as this means you can break everything — use at your own risk or pleasure! If you notice any exceptions or strange behavior in your middleware pipeline and are using any of the following, remove your custom middlewares and try again. .. _mi-ocelotpipelineconfiguration-class: ``OcelotPipelineConfiguration`` Class ------------------------------------- .. _OcelotPipelineConfiguration: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Middleware/OcelotPipelineConfiguration.cs Class: `OcelotPipelineConfiguration`_ The user can set middleware-functions aka custom user's middleware against the following: .. list-table:: :widths: 50 50 :header-rows: 1 * - *Middleware* - *Description* * - | ``PreErrorResponderMiddleware`` | Prev: ``ExceptionHandlerMiddleware`` | Next: ``ResponderMiddleware`` - This is called after the global error-handling middleware, so any code before calling ``next.Invoke`` is the next action executed in the Ocelot pipeline. Any code after ``next.Invoke`` is the final action executed in the Ocelot pipeline before reaching the global error handler. * - | ``ResponderMiddleware`` | Prev: ``PreErrorResponderMiddleware`` | Next: ``DownstreamRouteFinderMiddleware`` - This allows the user to completely override Ocelot's `ResponderMiddleware `_. :sup:`1` * - | ``PreAuthenticationMiddleware`` | Prev: ``RequestIdMiddleware`` | Next: ``AuthenticationMiddleware`` - This allows the user to run any extra authentication before the Ocelot authentication kicks in. * - | ``AuthenticationMiddleware`` | Prev: ``PreAuthenticationMiddleware`` | Next: ``ClaimsToClaimsMiddleware`` - This allows the user to completely override Ocelot's `AuthenticationMiddleware `_. :sup:`1` * - | ``PreAuthorizationMiddleware`` | Prev: ``ClaimsToClaimsMiddleware`` | Next: ``AuthorizationMiddleware`` - This allows the user to run any extra authorization before the Ocelot authorization kicks in. * - | ``AuthorizationMiddleware`` | Prev: ``PreAuthorizationMiddleware`` | Next: ``ClaimsToHeadersMiddleware`` - This allows the user to completely override Ocelot's `AuthorizationMiddleware `_. :sup:`1` * - | ``ClaimsToHeadersMiddleware`` | Prev: ``AuthorizationMiddleware`` | Next: ``PreQueryStringBuilderMiddleware`` - This allows the user to completely override Ocelot's `ClaimsToHeadersMiddleware `_. :sup:`1` * - | ``PreQueryStringBuilderMiddleware`` | Prev: ``ClaimsToHeadersMiddleware`` | Next: ``ClaimsToQueryStringMiddleware`` - This allows the user to implement own query string manipulation logic. Obviously, you can add the mentioned Ocelot middleware overrides as normal before the call to ``app.UseOcelot``. They cannot be added afterward because Ocelot does not invoke subsequent middleware overrides based on the specified middleware configuration. As a result, the next-called middleware **will not** affect the Ocelot configuration. .. warning:: :sup:`1` Use the mentioned middleware overrides with caution! Overridden middleware removes the default implementation. If you encounter any exceptions or strange behavior in your middleware pipeline, remove the overridden middleware and try again. .. _mi-ocelot-pipeline-builder: Ocelot Pipeline Builder ----------------------- | Class: ``Ocelot.Middleware.OcelotPipelineExtensions`` | Method: ``BuildOcelotPipeline(IApplicationBuilder, OcelotPipelineConfiguration)`` The Ocelot pipeline is part of the entire `ASP.NET Core Middleware `_ conveyor, also known as the app pipeline. The `BuildOcelotPipeline `_ method encapsulates the Ocelot pipeline. The last middleware in the ``BuildOcelotPipeline`` method is ``HttpRequesterMiddleware``, which calls the next middleware if it is added to the pipeline. The internal `HttpRequesterMiddleware `_ is part of the pipeline, but it is private and cannot be overridden since this middleware is not included in the list of `user-accessible public middlewares `_ that can be overridden. Therefore, it is the `final middleware `_ in both the Ocelot and ASP.NET pipelines, and it handles non-user operations. The last user (public) middleware that can be overridden is `PreQueryStringBuilderMiddleware `_, which is read from the pipeline configuration object. For more details, see the previous :ref:`mi-ocelotpipelineconfiguration-class` section. To understand the actual order of middleware execution, here is a quick list of them, with an asterisk (*) marking the ones that can be overridden: 1. ``ConfigurationMiddleware`` 2. ``ExceptionHandlerMiddleware`` 3. ``PreErrorResponderMiddleware``\* 4. ``ResponderMiddleware``\* 5. ``DownstreamRouteFinderMiddleware`` 6. ``MultiplexingMiddleware`` 7. ``SecurityMiddleware`` 8. ``HttpHeadersTransformationMiddleware`` 9. ``DownstreamRequestInitialiserMiddleware`` 10. ``RateLimitingMiddleware`` 11. ``RequestIdMiddleware`` 12. ``PreAuthenticationMiddleware``\* 13. ``AuthenticationMiddleware``\* 14. ``ClaimsToClaimsMiddleware`` 15. ``PreAuthorizationMiddleware``\* 16. ``AuthorizationMiddleware``\* 17. ``ClaimsToHeadersMiddleware``\* 18. ``PreQueryStringBuilderMiddleware``\* 19. ``ClaimsToQueryStringMiddleware`` 20. ``ClaimsToDownstreamPathMiddleware`` 21. ``LoadBalancingMiddleware`` 22. ``DownstreamUrlCreatorMiddleware`` 23. ``OutputCacheMiddleware`` 24. ``HttpRequesterMiddleware`` Considering that ``PreQueryStringBuilderMiddleware`` and ``HttpRequesterMiddleware`` are the final user and system middleware, there are no other middleware components in the pipeline. However, you can still extend the ASP.NET pipeline, as demonstrated in the following code: .. code-block:: csharp await app.UseOcelot(); app.UseMiddleware(); However, we do not recommend adding custom middleware before or after calling ``UseOcelot()`` because it affects the stability of the entire pipeline and has not been tested. This type of custom pipeline building falls outside the Ocelot pipeline model, and the quality of the solution is your responsibility. Finally, do not confuse the distinction between system (private, non-overridden) and user (public, overridden) middleware. Private middleware is hidden and cannot be overridden, but the entire ASP.NET pipeline can still be extended. The public middleware of the :ref:`mi-ocelotpipelineconfiguration-class` is fully customizable and can be overridden. Roadmap ------- The community has shown interest in adding more overridden middleware. One such request is pull request `1497 `_, which may possibly be included in an upcoming release. In any case, if the current overridden middleware does not provide enough pipeline flexibility, you can open a new topic in the `Discussions `_ of the repository. |octocat| .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle ================================================ FILE: docs/features/qualityofservice.rst ================================================ .. role:: htm(raw) :format: html .. role:: pdf(raw) :format: latex pdflatex .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs .. _Polly: https://www.pollydocs.org .. _documentation: https://www.pollydocs.org .. _Resilience strategies: https://www.pollydocs.org/strategies/index.html .. |QoS_label| image:: https://img.shields.io/badge/-QoS-D3ADAF.svg :target: https://github.com/ThreeMammals/Ocelot/labels/QoS :alt: label QoS :class: img-valign-textbottom Quality of Service ================== Repository Label: |QoS_label|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/labels/QoS}{QoS}` Ocelot currently supports a single *Quality of Service* (QoS) capability. It allows you to configure, on a per-route basis, the application of a circuit breaker when making requests to downstream services. This feature leverages a well-regarded .NET library known as `Polly`_. For more details, visit the `Polly`_ library's official `repository `_. .. note:: `Polly`_ v7 syntax is no longer supported as of version `23.2`_, when the Ocelot team upgraded Polly `from v7 to v8 `_. Installation ------------ To utilize the *Quality of Service* via `Polly`_ library, begin by importing the appropriate `Ocelot.Provider.Polly `_ extension package: .. code-block:: powershell Install-Package Ocelot.Provider.Polly Next, in your `Program`_, incorporate `Polly`_ services by invoking the ``AddPolly()`` extension on the ``OcelotBuilder``, as shown below [#f1]_: .. code-block:: csharp :emphasize-lines: 5 using Ocelot.Provider.Polly; builder.Services .AddOcelot(builder.Configuration) .AddPolly(); .. _qos-schema: ``QoSOptions`` Schema --------------------- .. _MinimumThroughput: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput .. _BreakDuration: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration .. _FailureRatio: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio .. _SamplingDuration: https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration .. _Timeout: https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout .. _FileQoSOptions: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileQoSOptions.cs Class: `FileQoSOptions`_ Here is the complete *Quality of Service* configuration, also known as the "QoS options schema". Depending on your needs and choosen strategies definition of all properties are not required. If you skip a property then a default value will be substituted as per Ocelot/Polly specification. .. code-block:: json "QoSOptions": { // Circuit Breaker strategy "BreakDuration": 0, // integer "MinimumThroughput": 0, // integer "FailureRatio": 0.0, // floating number "SamplingDuration": 0, // integer // Timeout strategy "Timeout": 0, // integer // Deprecated options "DurationOfBreak": 0, // deprecated! -> use BreakDuration "ExceptionsAllowedBeforeBreaking": 0, // deprecated! -> use MinimumThroughput "TimeoutValue": 0, // deprecated! -> use Timeout } .. list-table:: :widths: 30 70 :header-rows: 1 * - *Ocelot Option and Polly equivalent* - *Description* * - ``BreakDuration`` (formerly ``DurationOfBreak``) as `BreakDuration`_ - This is duration of break the circuit will stay open before resetting. The unit is milliseconds. * - ``MinimumThroughput`` (formerly ``ExceptionsAllowedBeforeBreaking``) as `MinimumThroughput`_, a primary option - This number of actions or more must pass through the circuit within the time slice for the statistics to be considered significant and for the circuit breaker to engage * - ``FailureRatio`` is `FailureRatio`_ - This is the failure-to-success ratio at which the circuit will break * - ``SamplingDuration`` is `SamplingDuration`_ - This is the duration of the sampling over which failure ratios are assessed. The unit is milliseconds. * - ``Timeout`` (formerly ``TimeoutValue``) as `Timeout`_, a primary option - This is the default timeout. The unit is milliseconds. .. warning:: The following options are deprecated in version `24.1`_: ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue``! Use the appropriate new options as shown in the table above. These deprecated options will be removed in version `25.0`_. For backward compatibility in version `24.1`_, a deprecated option takes precedence over its replacement. .. _break1: http://break.do **Note** [#f2]_: Ocelot checks that the values of options are valid during execution. If not, it logs errors or warnings (refer to the :ref:`qos-notes-value-constraints` section in :ref:`qos-notes`). For a complete explanation about strategies and mechanisms, consult Polly's `Resilience strategies`_ documentation. .. _qos-global-configuration: Global Configuration [#f3]_ --------------------------- According to the :ref:`config-global-configuration-schema`, global *Quality of Service* options for static routes were introduced in version `24.1`_. These global options can also be overridden in the ``Routes`` configuration section, a capability that has been supported for a long time. .. code-block:: json :emphasize-lines: 5-7, 12, 18-21 { "Routes": [ { "Key": "R0", // optional "QoSOptions": { "Timeout": 15000 // 15s }, // ... }, { "Key": "R1", // this route is part of a group "QoSOptions": {}, // optional due to grouping // ... } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "QoSOptions": { "RouteKeys": ["R1",], // if undefined or empty array, opts will apply to all routes "BreakDuration": 1000, // 1s "MinimumThroughput": 3 }, // ... } } Dynamic routes were not supported in versions prior to `24.1`_. However, global *Quality of Service* options have been available in :ref:`Dynamic Routing ` mode for a long time. Starting with version `24.1`_, global *QoS* options can also be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`. .. code-block:: json :emphasize-lines: 6-8, 17-22 { "DynamicRoutes": [ { "Key": "", // optional "ServiceName": "my-service", "QoSOptions": { "Timeout": 15000 // 15s }, } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "DownstreamScheme": "http", "ServiceDiscoveryProvider": { // required section for dynamic routing }, "QoSOptions": { "RouteKeys": [], // or null, no grouping, thus opts apply to all dynamic routes "BreakDuration": 1000, // 1s "MinimumThroughput": 3, "FailureRatio": 0.1, // 10% "SamplingDuration": 30000 // 30s } } } In this dynamic routing configuration, the :ref:`qos-timeout-strategy` is applied to the ``my-service`` service in addition to the :ref:`qos-circuit-breaker-strategy`, resulting in `Polly`_ timing out after 15 seconds. However, for all implicit dynamic routes, the :ref:`qos-timeout-strategy` is not globally configured, in favor of the standard :ref:`config-timeout` option managed by the Ocelot Core requester middleware. Lastly, the :ref:`qos-circuit-breaker-strategy` has been globally configured for all routes due to the absence of route grouping, with the following options: allow 3 errors before breaking the circuit for 1 second, and allow up to 10% errors during the default 30-second sampling period. .. note:: 1. Please note that route-level options take precedence over global options. 2. If the ``RouteKeys`` option is not defined or the array is empty in the global ``QoSOptions``, the global options will apply to all routes. If the array contains route keys, it defines a single group of routes to which the global options apply. Routes excluded from this group must specify their own route-level ``QoSOptions``. 3. Since Ocelot's Polly provider utilizes the `Resilience pipeline registry`_, each route has a dedicated pipeline cached in Polly's registry using the route's load-balancing key. For a static route, the load-balancing key uniquely identifies the route by its upstream options, whereas for dynamic routes the load-balancing key is typically the service name from the discovery provider. Thus, Polly's registry maintains dedicated pipelines for each discovered service, and those pipelines behave independently. Finally, it is important to understand that global *QoS* options do not create a single shared resilience pipeline in the registry. 4. Dynamic routes were not supported in versions prior to `24.1`_. Beginning with version `24.1`_, global *QoS* options for :ref:`Dynamic Routing ` may be overridden in the ``DynamicRoutes`` configuration section, as defined by the :ref:`config-dynamic-route-schema`. Additionally, global configuration for static routes (also known as ``Routes``) has been supported since version `24.1`_. .. _Resilience pipeline registry: https://www.pollydocs.org/pipelines/resilience-pipeline-registry.html .. _qos-circuit-breaker-strategy: Circuit Breaker strategy ------------------------ .. _Circuit breaker resilience strategy: https://www.pollydocs.org/strategies/circuit-breaker.html | Documentation: `Circuit breaker resilience strategy`_ | Primary option: ``MinimumThroughput``, formerly ``ExceptionsAllowedBeforeBreaking`` The options ``MinimumThroughput`` and ``BreakDuration`` can be configured independently from ``Timeout``: .. code-block:: json "QoSOptions": { "MinimumThroughput": 3, "BreakDuration": 1000 // ms } Alternatively, you can omit ``BreakDuration``, which will default to the implicit 5-second setting as specified in Polly's `BreakDuration`_ documentation: .. code-block:: json "QoSOptions": { "MinimumThroughput": 3 } This setup activates only the `Circuit breaker resilience strategy`_. Additionally, there is a failure handling strategy based on ``FailureRatio``, which serves as a counterpart to, or supplement for, the number of failures, also known as ``MinimumThroughput``. .. code-block:: json "QoSOptions": { "MinimumThroughput": 10, "FailureRatio": 0.5, // 50% "SamplingDuration": 10000, // ms, 10 seconds } Thus, a failure ratio of ``0.5`` indicates that the circuit will break if 50% or more of actions result in handled failures, after reaching the minimum threshold of 10 failures, also known as the ``MinimumThroughput`` option. Additionally, the 10-second sampling duration defines the time window over which the 50% failure ratio is evaluated. **Note**: The ``MinimumThroughput`` option (also known as Polly's `MinimumThroughput`_) is the primary option that enables the *Circuit Breaker strategy*. Its value must be valid (set to 2 or greater, refer to the :ref:`qos-notes-value-constraints` section in :ref:`qos-notes`) and may be supplemented with additional Circuit Breaker options. .. _qos-timeout-strategy: Timeout strategy ---------------- .. _Timeout resilience strategy: https://www.pollydocs.org/strategies/timeout.html | Documentation: `Timeout resilience strategy`_ | Primary option: ``Timeout``, formerly ``TimeoutValue`` The ``Timeout`` can be configured independently from the options of the :ref:`qos-circuit-breaker-strategy`: .. code-block:: json "QoSOptions": { "Timeout": 5000 // ms } This setup activates only the `Timeout resilience strategy`_. To configure a global QoS timeout using the *Timeout strategy* for all routes (both static and dynamic) set the ``Timeout`` option as defined in the :ref:`config-global-configuration-schema`: .. code-block:: json "GlobalConfiguration": { // other global props "QoSOptions": { "Timeout": 10000 // ms, 10 seconds } } Please note that the route-level timeout takes precedence over the global timeout. For example, a route timeout may be shorter, while the global timeout can be longer and apply to all routes. **Note**: There are :ref:`qos-notes-value-constraints` for ``Timeout``: it must be a positive number starting from *1 millisecond* to enable the *Timeout strategy*. If ``Timeout`` is undefined, zero or a negative number, the *Timeout strategy* will not be added to the resilience pipeline. Also, keep in mind Polly's `Timeout`_ constraint, thus Ocelot validates the ``Timeout``. If the value violates Polly's requirements, it will be rolled back to the default of *30 seconds*. .. _qos-notes: Notes ----- .. _DefTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22const+int+DefTimeout%22&type=code .. _DefaultTimeoutSeconds: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+%22static+int+DefaultTimeoutSeconds%22&type=code .. _DefaultTimeout: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+DefaultTimeout+path%3A%2F%5Esrc%5C%2FOcelot.Provider.Polly%5C%2F%2F&type=code .. _qos-notes-absolute-timeout: Absolute timeout [#f4]_ ^^^^^^^^^^^^^^^^^^^^^^^ If a *QoS* section is not included, *QoS* will not be applied, and Ocelot will enforce an absolute timeout of 90 seconds (defined by the ``DownstreamRoute`` `DefTimeout`_ constant) for all downstream requests. This absolute timeout is configurable via the ``DownstreamRoute`` `DefaultTimeoutSeconds`_ static C# property. For more information, refer to the :ref:`config-default-timeout` section of the :doc:`../features/configuration` chapter. .. _qos-notes-value-constraints: Value constraints ^^^^^^^^^^^^^^^^^ Starting with `Polly`_ v8, the `Resilience strategies`_ documentation outlines the following constraints on values: * The ``BreakDuration`` value must exceed **500** milliseconds and be less than **24** hours (1 day = ``86 400 000`` milliseconds). If unspecified or invalid, it defaults to **5000** milliseconds (5 seconds); refer to the `BreakDuration`_ documentation. * The ``MinimumThroughput`` value must be **2** or greater. If unspecified or invalid, it defaults to **100** failures; refer to the `MinimumThroughput`_ documentation. * The ``FailureRatio`` must be greater than **0.0** and no more than **1.0**. If unspecified or invalid, it defaults to **0.1** (10%); refer to the `FailureRatio`_ documentation. * The ``SamplingDuration`` value must exceed **500** milliseconds and be less than **24** hours (1 day = ``86 400 000`` milliseconds). If unspecified or invalid, it defaults to **30000** milliseconds (30 seconds); refer to the `SamplingDuration`_ documentation. * The ``Timeout`` must be greater than **10** milliseconds and less than **24** hours (1 day = ``86 400 000`` milliseconds). If unspecified or invalid, it defaults to **30000** milliseconds (30 seconds); refer to the `Timeout`_ documentation. And please note, when both route-level and global *QoS* timeouts have positive values but are invalid, a default value will be automatically substituted from the ``TimeoutStrategy`` class `DefaultTimeout`_ static C# property, which can also be configured in your `Program`_. Ocelot logs warnings containing failed validation messages for all options, but it does not block Ocelot startup, even when *QoS* options are invalid. Inspect your logs for these messages and adjust your configuration if necessary. .. _qos-notes-qos-and-route-global-timeouts: QoS and route (global) timeouts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``Timeout`` option in *QoS* always takes precedence over the route :ref:`config-timeout` property, so :ref:`config-timeout` will be ignored in favor of QoS ``Timeout``. In Ocelot Core, ``Timeout`` and configuration :ref:`config-timeout` are not intended to be used together. Moreover, there is an Ocelot Core design constraint: if the route or global ``Timeout`` duration is shorter than the *QoS* ``Timeout``, you may encounter warning messages in the logs that begin with the following sentence: .. code-block:: text Route '/xxx' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: ... This warning means that the route or global timeout will occur before the *QoS* :ref:`qos-timeout-strategy` has a chance to handle its own timeout event, which is configured with a longer duration. Technically, this situation results in the functional disabling of the Polly's `Timeout resilience strategy`_. Ocelot handles this misconfiguration by logging a warning and automatically applying a longer timeout to the ``TimeoutDelegatingHandler`` in order to effectively unblock the *QoS* :ref:`qos-timeout-strategy`. To avoid this warning, ensure that your *QoS* timeouts are shorter than the route or global timeouts, or remove the :ref:`config-timeout` property from routes where *QoS* is enabled with the ``Timeout`` option. .. _qos-notes-global-and-default-qos-timeouts: Global and default QoS timeouts ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If a route-level *QoS* timeout is undefined, the global ``Timeout`` takes precedence over the default timeout (30 seconds, see the `Timeout`_ docs). This means the global *QoS* timeout can override Polly's default of `30 seconds `_ via the :ref:`config-global-configuration-schema`. .. _qos-extensibility: Extensibility [#f5]_ -------------------- To use your ``ResiliencePipeline`` provider, you can apply the following syntax: .. code-block:: csharp :emphasize-lines: 3 builder.Services .AddOcelot(builder.Configuration) .AddPolly(); // MyProvider should implement IPollyQoSResiliencePipelineProvider // Note: you can use standard provider PollyQoSResiliencePipelineProvider Additionally, if you want to utilize your own ``DelegatingHandler``, the following syntax can be applied: .. code-block:: csharp :emphasize-lines: 3 builder.Services .AddOcelot(builder.Configuration) .AddPolly(MyQosDelegatingHandlerDelegate); // MyQosDelegatingHandlerDelegate is a delegate use to get a DelegatingHandler. Refer to Ocelot's PollyResiliencePipelineDelegatingHandler Finally, to define your own set of exceptions for mapping, you can apply the following syntax: .. code-block:: csharp :emphasize-lines: 11 static Error CreateError(Exception e) => new RequestTimedOutError(e); Dictionary> MyErrorMapping = new() { {typeof(TaskCanceledException), CreateError}, {typeof(TimeoutRejectedException), CreateError}, {typeof(BrokenCircuitException), CreateError}, {typeof(BrokenCircuitException), CreateError}, }; builder.Services .AddOcelot(builder.Configuration) .AddPolly(MyErrorMapping); // Note: Default error mapping is defined in the DefaultErrorMapping field of the Ocelot.Provider.Polly.OcelotBuilderExtensions class """" .. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. .. [#f2] If something doesn't work or you're stuck, consider reviewing the current `QoS issues `_ filtered by the |QoS_label| label. .. [#f3] The :ref:`Global Configuration ` for dynamic routes was first introduced in pull request `351`_ and released in version `7.0.1`_. Since then, global configuration for static routes was added in pull requests `2081`_ and `2339`_, and delivered in version `24.1`_. Support for dynamic routes was also added in pull request `2339`_ and delivered in version `24.1`_. .. [#f4] The :ref:`Absolute timeout ` configuration, used as the :ref:`config-default-timeout`, and the :ref:`config-timeout` feature were requested in issue `1314`_, implemented in pull request `2073`_, and officially released in version `24.1`_. .. [#f5] The :ref:`Extensibility ` feature was requested in issue `1875`_ and implemented through pull request `1914`_, as part of version `23.2`_. .. _351: https://github.com/ThreeMammals/Ocelot/pull/351 .. _1314: https://github.com/ThreeMammals/Ocelot/issues/1314 .. _1875: https://github.com/ThreeMammals/Ocelot/issues/1875 .. _1914: https://github.com/ThreeMammals/Ocelot/pull/1914 .. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073 .. _2081: https://github.com/ThreeMammals/Ocelot/pull/2081 .. _2339: https://github.com/ThreeMammals/Ocelot/pull/2339 .. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1 .. _23.2: https://github.com/ThreeMammals/Ocelot/releases/tag/23.2.0 .. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0 ================================================ FILE: docs/features/ratelimiting.rst ================================================ Rate Limiting ============= Feature label: `Rate Limiting `_ Handy articles: * `What is rate limiting? | Microsoft Cloud | Microsoft Learn `_ * `Rate Limiting pattern | Azure Architecture Center | Microsoft Learn `_ * `Rate limit an HTTP handler in .NET | .NET | Microsoft Learn `_ * `Rate limiting middleware in ASP.NET Core | Microsoft Learn `_ Ocelot implements *rate limiting* [#f1]_ for upstream requests to prevent downstream services from being overwhelmed. .. _rl-schema: ``RateLimitOptions`` Schema --------------------------- .. _FileRateLimitByHeaderRule: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileRateLimitByHeaderRule.cs .. _FileGlobalRateLimitByHeaderRule: https://github.com/ThreeMammals/Ocelot/blob/main/src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs .. _503 Service Unavailable: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503 Class: `FileRateLimitByHeaderRule`_ As you may already know from the :doc:`../features/configuration` chapter and the :ref:`config-route-schema` and :ref:`config-dynamic-route-schema` sections, there is a special ``RateLimitOptions`` object schema for routes: .. code-block:: json "RateLimitOptions": { // rule, partition by "ClientIdHeader": "", "ClientWhitelist": [""], // management opts "EnableRateLimiting": true, "EnableHeaders": true, // algorithm "Limit": 1, "Period": "", "Wait": "", // extended opts "StatusCode": 1, "QuotaMessage": "", "KeyPrefix": "" } Additionally, the :ref:`config-global-configuration-schema` allows configuring global *Rate Limiting* options. **Note 1**: The complete route-level ``RateLimitOptions`` schema, including all available properties, is defined in the C# `FileRateLimitByHeaderRule`_ class. The global ``RateLimitOptions`` schema includes an additional ``RouteKeys`` array option, which allows grouping routes to which the global options will apply (refer to the C# `FileGlobalRateLimitByHeaderRule`_ class for details). If the ``RouteKeys`` option is not defined in the global ``RateLimitOptions``, the global settings will apply to all routes. **Note 2**: You do not need to set all of these options due to default values, but the following rule options are required: ``Limit`` and ``Period``. If these required options are undefined and no global configuration is present, Ocelot will fail to start due to an internally generated validation error, which will be visible in the logs. **Note 3**: Several :ref:`deprecated options ` originating from version `24.0`_ and earlier (see `old schema`_) are retained for one release cycle. Both introduced and :ref:`deprecated options ` are detailed in the :ref:`rl-configuration` table below. .. _rl-configuration: Configuration [#f2]_ -------------------- A complete configuration consists of both route-level and global *Rate Limiting*. You can configure the following options in the ``GlobalConfiguration`` section of `ocelot.json`_: .. code-block:: json "Routes": [ { "Key": "R1", "RateLimitOptions": { "ClientWhitelist": ["ocelot-client1-preshared-key"], "Limit": 1000, "Period": "20s", // (milli)seconds, minutes, hours, days "Wait": "1.5m" // (milli)seconds, minutes, hours, days "StatusCode": 418, // I'm a teapot -> this is special status "QuotaMessage": "Out of coffee! Our bar can only serve up to {0} cups of coffee every {1}. In the meantime, why not grab some tea and relax for Retry-After seconds until we're ready to serve again?" } } ], "GlobalConfiguration": { "BaseUrl": "https://api.ocelot.net", "RateLimitOptions": { "RouteKeys": ["R1"], // if undefined or empty array, opts will apply to all routes "ClientIdHeader": "Oc-Client", // std (default) header name "Limit": 100, "Period": "30s", // ms, s, m, h, d "Wait": "1m", // ms, s, m, h, d "StatusCode": 429, // Too Many Requests -> standard status "QuotaMessage": "Ocelot API calls quota exceeded! Maximum admitted {0} per {1}.", // standard template with 2 parameters "KeyPrefix": "ocelot-rate-limiting" // for caching key } } .. list-table:: :widths: 25 75 :header-rows: 1 * - :ref:`Schema ` Option - Description * - ``ClientIdHeader`` - Specifies the header used to identify clients, with "Oc-Client" set as the default. * - ``ClientWhitelist`` - An array that contains the clients exempt from *rate limiting*. * - ``EnableRateLimiting`` - Enables or disables rate limiting. Defaults to ``true`` (enabled). * - ``EnableHeaders`` - Specifies whether the ``X-RateLimit-*`` and ``Retry-After`` headers are enabled. If undefined, defaults to ``true`` (enabled). * - ``Limit`` - The maximum number of requests a client can make within a given time ``Period``. * - ``Period`` - Rate limiting period (fixed window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d). If the exact ``Limit`` of requests is reached (quota exceeded\*), the request is immediately blocked, and if ``Wait`` is defined, a waiting period begins. * - ``Wait`` - Rate limiting wait window (no servicing period) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d). This option can have shorter or longer durations compared to the fixed window duration specified as ``Period``. The waiting interval either extends or shortens the Quota Exceeded period\*, which typically ends after the fixed window elapses. * - ``StatusCode`` - The rejection status code returned during the Quota Exceeded period\*. Default value: 429 (`Too Many Requests`_). * - ``QuotaMessage`` - Specifies the message displayed when the quota is exceeded. The value to be used as the formatter for the Quota Exceeded\* response message. If none specified the default will be informative. * - ``KeyPrefix`` - The counter prefix, used to compose the rate limiting counter caching key to be used by the ``IRateLimitStorage`` service. Default value: "Ocelot.RateLimiting" .. admonition:: "Quota Exceeded period" term The **Quota Exceeded period** refers to the ``Wait`` window, if defined, or the remaining duration of the fixed ``Period`` following the moment the request ``Limit`` is exceeded. During this time, the configured rejection ``StatusCode`` is returned, and the formatted ``QuotaMessage`` is written to the response body. To determine when this period ends, clients should inspect the ``Retry-After`` header, which provides a floating-point value indicating the number of seconds until the next allocated fixed window begins. The ``X-RateLimit-*`` headers are included in the response during the *Quota Exceeded period*, provided that headers are enabled via the ``EnableHeaders`` option. .. _break: http://break.do **Note 1**: If the ``RouteKeys`` option is not defined or the array is empty in the global ``RateLimitOptions``, the global settings will apply to all routes. If the array contains route keys, it defines a single group of routes to which the global options apply. Routes excluded from this group must specify their own route-level ``RateLimitOptions``. **Note 2**: The string values for the ``Period`` and ``Wait`` options must contain a floating-point number followed by one of the supported time units: 'ms', 's', 'm', 'h', or 'd'. If no unit is specified, the value defaults to milliseconds. For example, "333.5" is interpreted as 333 milliseconds and 500 microseconds (equivalent to "333.5ms"). The floating-point component may be omitted; for example, "10.0s" is equivalent to "10s". These values are parsed dynamically at runtime, so the required ``Period`` option in `ocelot.json`_ is validated early through fluent validation when the Ocelot app starts. If an invalid value is provided, the *Rate Limiting* middleware will throw a ``FormatException``, which is logged accordingly. .. _rl-deprecated-options: Deprecated options [#f3]_ ^^^^^^^^^^^^^^^^^^^^^^^^^ .. warning:: Here are the deprecated options from the `old schema`_: .. list-table:: :widths: 30 70 :header-rows: 1 * - *Deprecated and Introduced Options* - *Description* * - ``DisableRateLimitHeaders`` and ``EnableHeaders`` - Specifies whether the ``X-RateLimit-*`` and ``Retry-After`` headers are disabled. * - ``PeriodTimespan`` and ``Wait`` - This parameter specifies the time, in **seconds**, after which a retry is allowed. During this interval, the ``QuotaExceededMessage`` will be included in the response, along with the corresponding ``HttpStatusCode``. Clients are encouraged to refer to the ``Retry-After`` header to determine when subsequent requests can be made. * - ``HttpStatusCode`` and ``StatusCode`` - Specifies the HTTP status code returned during *rate limiting*, with a default value of **429** (`Too Many Requests`_). * - ``QuotaExceededMessage`` and ``QuotaMessage`` - Specifies the message displayed when the quota is exceeded. This option is optional, and the default message is informative. * - ``RateLimitCounterPrefix`` and ``KeyPrefix`` - Specifies the counter prefix used to construct the *rate limiting* counter cache key. Notes ----- .. note:: 1. Prior to version `24.1`_, global options were only accessible in the special :ref:`Dynamic Routing ` mode. Since version `24.1`_, global configuration has been available for both static and dynamic routes. As a team, we would consider the idea of implementing such a global configuration for aggregated routes. However, an aggregated route is essentially a combination of static routes. 2. Global *rate limiting* options may not be practical as they apply limits to all routes. In a microservices architecture, it is unusual to enforce the same limitations across all routes. Configuring per-route *rate limiting* could offer a more tailored solution. However, global *rate limiting* can be logical if all routes share the same downstream hosts, thereby restricting the usage of a single service or a single product. 3. The ``DisableRateLimitHeaders`` option is deprecated as of version `24.1`_. Use ``EnableHeaders`` instead, applying boolean value negation as needed. If ``DisableRateLimitHeaders`` is defined, it takes precedence; otherwise, ``EnableHeaders`` will be used. Do not define both options. This setting is retained for backward compatibility but is subject to change. Therefore, the ``DisableRateLimitHeaders`` option will be removed in the upcoming major release, version `25.0`_. The same applies to other :ref:`deprecated options `. 4. Ocelot's own *rate limiting* does not utilize built-in ASP.NET Core features, so it is not based on the "`Rate limiting middleware in ASP.NET Core`_" described in the :ref:`rl-roadmap` below. The Ocelot team believes that the ASP.NET Core rate limiting middleware enables global limitations through its rate-limiting policies. .. _rl-algorithms: Algorithms ---------- The currently implemented rate limiter algorithms in Ocelot are: - **Fixed window**: Based on the ``Period`` option, without the ``Wait`` option (previously known as the deprecated ``PeriodTimespan``). - **Hybrid fixed window**: The combination of ``Period`` and ``Wait`` enables fixed-window-like behavior with additional control over the duration and handling of the *"Quota Exceeded period"*. Historically, Ocelot's rate limiting algorithm was a hybrid, combining the classic "fixed window" approach with a waiting no-service period. Since version `24.1`_, the hybrid algorithm has been split into two distinct algorithms, allowing the classic "fixed window" to be used independently. To understand the terminology, please refer to the Handy Articles listed at the beginning of this chapter. For beginners, here is a quick link: `Announcing ASP.NET Core rate limiting algorithms`_. For professionals, we recommend reading the official Microsoft Learn article "`Rate limiting middleware in ASP.NET Core`_", especially the `Rate Limiter Algorithms`_ section, and/or searching the internet for additional resources. **Note 1**: Ocelot's own rate limiter does not implement other classic algorithms such as "Sliding Window", "Token Bucket", or "Concurrency". However, these algorithms are outlined in the :ref:`rl-roadmap`. **Note 2**: Ocelot's own rate limiter does not manage concurrent HTTP requests via a queue. Therefore, all concurrency handling and decision-making should be implemented on the client side using classic retry patterns to ensure quality of service. The management strategy is deliberately simple: *First-In means First Wins*. If the first request acquires a virtual lease from the limiting quota and the quota is immediately exceeded, the second request will be rejected with a 429 `Too Many Requests`_ response. .. _Announcing ASP.NET Core rate limiting algorithms: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/#what-is-rate-limiting:~:text=There%20are%20multiple%20different%20rate%20limiting%20algorithms%20to%20control%20the%20flow%20of%20requests. .. _Rate limiter algorithms: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0#rate-limiter-algorithms .. _Rate limiting middleware in ASP.NET Core: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit Rules (Partitions) ------------------ .. _API Key partition: https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-9.0#by-api-key Ocelot's rate limiting *rule* is a superset of the configuration options used to set up rate-limited access to a route. It enables partitioned rate limiting by processing the following artifacts through distinct stages: the client's identifier, a dedicated partition counter (quota), rate limiter algorithms, and the quota-exceeded response behavior. By Client's Header ^^^^^^^^^^^^^^^^^^ | Class: `FileRateLimitByHeaderRule`_ | JSON: :ref:`rl-schema` Currently, Ocelot's own rate limiting middleware supports and processes only the *"By Client's Header"* rule (partition), commonly referred to as the "`API Key partition`_" in ASP.NET Core terminology. Ocelot's rate limiting architecture provides dedicated subpartitions for each route, each with an independent counter for the rate limiter algorithm. Therefore, when client traffic enters the Ocelot pipeline, the current request is processed as follows: 1. Ocelot identifies the route by matching the URL path to the upstream route path, and allows the rate limiting middleware to process the client as part of the route partition. 2. Ocelot's rate limiting middleware creates the client's identity based on the configured *"By Client's Header"* rule and assigns a dedicated rate limiter counter to that client. 3. The rate limiting middleware executes the configured rate limiter algorithm, specifically the (hybrid) fixed window. Refer to the currently implemented :ref:`rl-algorithms` for details. 4. If the quota is exceeded, the rate limiting middleware returns appropriate "Quota Exceeded period" artifacts in the response, such as the status code, body message, and headers including ``Retry-After``. .. note:: If the client is not successfully identified, the rate limiting middleware blocks the request with a `503 Service Unavailable`_ status and writes an appropriate error message to the response body. Possible reasons for an empty identity include a missing header or an invalid ``ClientIdHeader`` value, as explained in the warning below. Whitelisted clients (defined via the ``ClientWhitelist`` option) are processed without limitation. .. warning:: Ocelot's rate limiting middleware is not responsible for validating API keys, also known as client header values, to be read from the configured header (``ClientIdHeader`` option). Users and developers must register these header values as pre-shared API keys on Ocelot's side and ensure they are validated before handing control over to the ``RateLimitingMiddleware``. We recommend implementing a custom middleware to validate API keys (client header values) and injecting it into the Ocelot pipeline using the :doc:`../features/middlewareinjection` feature. Specifically, the ``PreErrorResponderMiddleware`` (position 3) should be overridden, as it is invoked before the ``RateLimitingMiddleware`` at position 10. A more advanced solution may involve using the ``SecurityMiddleware`` at position 7, but in this case, users must implement their own ``ISecurityPolicy`` service and replace it in the :doc:`../features/dependencyinjection` (DI) container. To understand the Ocelot pipeline and its middleware positions, refer to the ":ref:`mi-ocelot-pipeline-builder`" documentation. .. _rl-roadmap: Roadmap ------- | Feature label: `Rate Limiting`_ | Development history: `Rate Limiting `__ [#f4]_ - **Rules**: The Ocelot team is considering a redesign of the *Rate Limiting* feature in light of the "`Announcing Rate Limiting for .NET`_" article by Brennan Conroy, published on July 13th, 2022. .. note:: Discover the new rate limiting functionality in ASP.NET Core: * The `RateLimiter Class `_, available since ASP.NET Core 7.0 * The `System.Threading.RateLimiting `_ NuGet package * The `Rate limiting middleware in ASP.NET Core`_ article by Arvin Kahbazi, Maarten Balliauw, and Rick Anderson As of now, the decision has been made to retain Ocelot's own `RateLimitingMiddleware`_ and extend it with an additional rule that will reference the attached ASP.NET Core rate limiting policy. This new rule is highly likely to be delivered in version `25.0`_, following the opening of pull request `2188`_. - **Algorithms**: In addition to the currently implemented hybrid "Fixed window" algorithm, which is built into Ocelot, the team plans to introduce other industry-standard algorithms, such as "Sliding window", "Token bucket", and "Concurrency, with priority given to "Sliding window" as the first. These lightweight algorithms should be easily configurable via JSON by end users who are not .NET developers, in order to avoid writing additional C# source code. Other interesting algorithms are welcome for discussion. We encourage you to share your thoughts with us in the `Discussions `_ of the repository. |octocat| Filter the current discussions by the `Rate Limiting `__ label. """" .. [#f1] Historically, the "`Rate Limiting <#rate-limiting>`__" feature is one of Ocelot's oldest and first features. This feature was introduced in pull request `37`_ and it was initially released in version `1.3.2`_. .. [#f2] Global :ref:`Configuration ` feature was introduced in pull request `2294`_ and delivered in version `24.1`_. .. [#f3] Several :ref:`deprecated options ` originating from version `24.0`_ and earlier (see `old schema`_) are retained for one release cycle. They are likely to be removed in the upcoming major release, version `25.0`_, which will include a significant upgrade to the *Rate Limiting* feature (refer to the :ref:`rl-roadmap`). The Ocelot team plans to implement an automatic configuration upgrade mechanism to support backward compatibility. However, we recommend reviewing the updated schema and beginning to adopt the new options. .. [#f4] Since pull request `37`_ and version `1.3.2`_, the Ocelot team has reviewed and redesigned the *Rate Limiting* feature. A fix for bug `1590`_ (pull request `1592`_) was released as part of version `23.3`_ to ensure stable behavior. Global :ref:`configuration ` support was introduced in pull request `2294`_ and delivered in version `24.1`_. .. _Announcing Rate Limiting for .NET: https://devblogs.microsoft.com/dotnet/announcing-rate-limiting-for-dotnet/ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main//samples/Basic/ocelot.json .. _article by @catcherwong: http://www.c-sharpcorner.com/article/building-api-gateway-using-ocelot-in-asp-net-core-rate-limiting-part-four/ .. _Too Many Requests: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 .. _old schema: https://github.com/ThreeMammals/Ocelot/blob/24.0.0/src/Ocelot/Configuration/File/FileRateLimitOptions.cs .. _RateLimitingMiddleware: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20RateLimitingMiddleware&type=code .. _37: https://github.com/ThreeMammals/Ocelot/pull/37 .. _1590: https://github.com/ThreeMammals/Ocelot/issues/1590 .. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 .. _2188: https://github.com/ThreeMammals/Ocelot/pull/2188 .. _2294: https://github.com/ThreeMammals/Ocelot/pull/2294 .. _1.3.2: https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2 .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _24.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13 .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle ================================================ FILE: docs/features/routing.rst ================================================ Routing ======= Ocelot's primary function is to handle incoming HTTP requests and forward them to a downstream service. Currently, Ocelot supports this only through HTTP requests. In the future, it might support other transport mechanisms. Ocelot defines the process of routing one request to another as a "Route". To make Ocelot functional, you must set up a *route* in its configuration. .. code-block:: json { "Routes": [] } To configure a *route*, you need to add one to the ``Routes`` JSON array. .. code-block:: json { "UpstreamHttpMethod": [ "Get", "Post" ], "UpstreamPathTemplate": "/posts/{postId}", "DownstreamPathTemplate": "/api/posts/{postId}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 80 } ] } The ``DownstreamPathTemplate``, ``DownstreamScheme``, and ``DownstreamHostAndPorts`` properties define the URL to which a request will be forwarded. The ``DownstreamHostAndPorts`` property is a collection that specifies the host and port of downstream services to which you intend to forward requests. Typically, it contains a single entry; however, in cases where *load balancing* is required, Ocelot allows you to add multiple entries and select an appropriate :doc:`../features/loadbalancer`. The ``UpstreamPathTemplate`` property specifies the URL that Ocelot uses to determine the appropriate ``DownstreamPathTemplate`` for a given request. The ``UpstreamHttpMethod`` property enables Ocelot to differentiate between requests with different HTTP verbs directed to the same URL. You can either specify a particular list of HTTP methods or leave the list empty to allow all methods. **Note**: The complete schema on a single *route* object is described in the :ref:`config-route-schema` section of the :doc:`../features/configuration` feature. .. _routing-placeholders: Placeholders ------------ In Ocelot, you can add placeholders for variables to your templates using the format of ``{something}``. The placeholder variable must be included in both the ``DownstreamPathTemplate`` and ``UpstreamPathTemplate`` properties. When present, Ocelot attempts to substitute the value of the placeholder from the ``UpstreamPathTemplate`` into the ``DownstreamPathTemplate`` for each request it processes. You can also do a :ref:`routing-catch-all` type of *route* e.g. .. code-block:: json { "UpstreamHttpMethod": [ "Get", "Post" ], "UpstreamPathTemplate": "/{everything}", "DownstreamPathTemplate": "/api/{everything}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 80 } ] } This will forward all path and query string combinations to the downstream service, appending them after the ``/api`` path. **Note**: The default routing configuration is **case-insensitive**. To change this, you can specify the following setting on a per-route basis: .. code-block:: json "RouteIsCaseSensitive": true This means that when Ocelot attempts to match an incoming upstream URL with an upstream template, the evaluation will be *case-sensitive*. .. _routing-embedded-placeholders: Embedded Placeholders [#f1]_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before version `23.4`_, Ocelot could not evaluate multiple placeholders embedded between two forward slashes (``/``). Additionally, it faced difficulties distinguishing placeholders from other elements within the slashes. For example, when the pattern ``/{url}-2/`` was applied to ``/y-2/``, it would produce ``{url}`` with ``y-2`` value. We have introduced an improved method for placeholder evaluation, making it easier to identify placeholders in complex URLs. **Example**: * **Path Pattern**: ``/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}`` * **Upstream URL Path**: ``/api/invoices_super/123-456_abcd/789?urlId=987`` * **Resulting Placeholders**: - ``{url0}`` = ``super`` - ``{url1}`` = ``123`` - ``{url2}`` = ``456`` - ``{url3}`` = ``789`` - ``{url4}`` = ``987`` .. _break: http://break.do **Note**, we believe this feature should be compatible with any URL query strings, although it has not been thoroughly tested. .. _routing-empty-placeholders: Empty Placeholders [#f2]_ ^^^^^^^^^^^^^^^^^^^^^^^^^ This represents a special edge case of :ref:`routing-placeholders`, in which the value of the placeholder is simply an empty string (``""``). For example, given the following *route* configuration: .. code-block:: json { "UpstreamPathTemplate": "/invoices/{url}", "DownstreamPathTemplate": "/api/invoices/{url}", } .. role:: htm(raw) :format: html This route works correctly when ``{url}`` is specified. For instance: * ``/invoices/123`` :htm:`→` ``/api/invoices/123`` **Edge Cases with Empty Placeholder Values**: 1. **Empty Placeholder**: When ``{url}`` is empty, the upstream path ``/invoices/`` routes to the downstream path ``/api/invoices/``. 2. **Omitting the Last Slash**: When the trailing slash is omitted, the upstream path ``/invoices`` should still route to the downstream path ``/api/invoices``. This behavior aligns intuitively with user expectations. .. _routing-catch-all: Catch All --------- Ocelot's *routing* supports a *"Catch All"* style, allowing users to specify routes that match all incoming traffic. If you configure your settings as shown below, all requests will be proxied directly. The placeholder ``{catchAll}`` is not significant, and any name can be used. .. code-block:: json { "UpstreamPathTemplate": "/{catchAll}", "DownstreamPathTemplate": "/{catchAll}", // ... } The *"Catch All"* route has a lower :ref:`priority ` than other routes. If the following route is included in your configuration, Ocelot will match it before the *"Catch All"* route. .. code-block:: json { "UpstreamPathTemplate": "/", "DownstreamPathTemplate": "/", // ... } .. _routing-priority: Priority [#f3]_ --------------- You can define the order in which your *routes* match the upstream URL by including a ``Priority`` property in the `ocelot.json`_ file. .. code-block:: json { "Priority": 0 } Priority **0** is the lowest *priority*. Ocelot always assigns ``0`` to :ref:`routing-catch-all` routes, and this value is still hardcoded. Beyond that, you are free to assign any *priority* you wish. e.g. you could have .. code-block:: json { "UpstreamPathTemplate": "/goods/{catchAll}", "Priority": 0 } and .. code-block:: json { "UpstreamPathTemplate": "/goods/delete", "Priority": 1 } In the example above, if a request is made to Ocelot on ``/goods/delete``, it will match the ``/goods/delete`` route. Previously, it would have matched ``/goods/{catchAll}``, as this was the first *route* in the list. Query Placeholders ------------------ In addition to URL path :ref:`routing-placeholders`, Ocelot can forward query string parameters, processing them in the form of ``{something}``. The query parameter placeholder must be included in both the ``DownstreamPathTemplate`` and ``UpstreamPathTemplate`` properties. Placeholder replacement works bi-directionally between paths and query strings, although it is subject to certain restrictions (see :ref:`routing-merging-of-query-parameters`). Path to Query String direction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ocelot allows you to include a query string as part of the ``DownstreamPathTemplate``, as demonstrated in the following example: .. code-block:: json { "UpstreamPathTemplate": "/api/units/{subscription}/{unit}/updates", "DownstreamPathTemplate": "/api/subscriptions/{subscription}/updates?unitId={unit}", } In this example, Ocelot uses the value of the ``{unit}`` placeholder from the upstream path template and includes it in the downstream request as a query string parameter named ``unitId``. **Note**: Ensure that the placeholder is named differently to account for the :ref:`routing-merging-of-query-parameters`. Query String to Path direction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Ocelot also allows you to include query string parameters in the ``UpstreamPathTemplate``, enabling you to match specific queries to corresponding services: .. code-block:: json { "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={uid}", "DownstreamPathTemplate": "/api/units/{subscriptionId}/{uid}/updates", } In this example, Ocelot matches only requests with a corresponding URL path where the query string begins with ``unitId=something``. Additional queries are permitted but must follow the matching parameter. Additionally, Ocelot replaces the ``{uid}`` parameter in the query string and incorporates it into the downstream request path. **Note**: The best practice is to use a placeholder name that differs from the name of the query parameter to accommodate the :ref:`routing-merging-of-query-parameters`. .. _routing-catch-all-query-string: Catch All Query String ^^^^^^^^^^^^^^^^^^^^^^ Ocelot's *routing* also supports a ":ref:`routing-catch-all`" style, allowing all query string parameters to be forwarded. The placeholder ``{query}`` is not significant, and any name can be used. .. code-block:: json { "UpstreamPathTemplate": "/contracts?{query}", "DownstreamPathTemplate": "/apipath/contracts?{query}", } This query string routing feature is particularly useful in scenarios where the query string needs to be routed without any transformations—for example, OData filters (see issue `1174`_). **Note**: The ``{query}`` placeholder can remain empty while catching all query strings, as this functionality is part of the ":ref:`Empty Placeholders `" feature [#f2]_. Consequently, upstream paths ``/contracts?`` and ``/contracts`` are routed to the downstream path ``/apipath/contracts``, with no query string attached. .. _routing-merging-of-query-parameters: Merging of Query Parameters ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Query string parameters are unsorted and merged to form the final downstream URL. This process is crucial because the ``DownstreamUrlCreatorMiddleware`` requires control over placeholder replacement and the merging of duplicate parameters. A parameter that appears first in the ``UpstreamPathTemplate`` may occupy a different position in the final downstream URL. Moreover, if the ``DownstreamPathTemplate`` includes query parameters at the beginning, their positions in the ``UpstreamPathTemplate`` will remain undefined unless explicitly specified. In a typical scenario, the merging algorithm constructs the final downstream URL query string as follows: 1. It begins by taking the initially defined query parameters in the ``DownstreamPathTemplate`` and placing them at the start, along with any necessary placeholder replacements. 2. Next, it adds all parameters from the :ref:`routing-catch-all-query-string`, represented by the placeholder ``{query}``, in the second position—following the explicitly defined parameters from *step 1*. 3. Finally, it appends any remaining replaced placeholder values as parameter values to the end of the query string, if present. Array parameters in ASP.NET API's model binding """"""""""""""""""""""""""""""""""""""""""""""" Due to the merging of parameters, ASP.NET API's special `model binding`_ for arrays does not support the array item representation format ``selectedCourses=1050&selectedCourses=2000``. This query string will be merged into ``selectedCourses=1050`` in the downstream URL, leading to the loss of array data. It is essential for upstream clients to generate the correct query string for array models, such as ``selectedCourses[0]=1050&selectedCourses[1]=2000``. For a detailed explanation of array model binding, refer to the documentation: "`Bind arrays and string values from headers and query strings`_". Control over parameter existence """""""""""""""""""""""""""""""" Be aware that query string placeholders are subject to naming restrictions due to the implementation of the ``DownstreamUrlCreatorMiddleware`` merging algorithm. Nevertheless, this restriction also offers flexibility in managing the presence of parameters in the final downstream URL based on their names. Consider the following two development scenarios :htm:`→` 1. A developer wishes **to preserve a parameter** after substituting a placeholder (refer to issue `473`_). This requires the use of the following template definition: .. code-block:: json { "UpstreamPathTemplate": "/path/{serverId}/{action}", "DownstreamPathTemplate": "/path2/{action}?server={serverId}" } | In this case, the ``{serverId}`` placeholder and the server parameter **names differ**. As a result, the ``server`` parameter is retained. | It is important to note that, due to the case-sensitive comparison of names, the ``server`` parameter will not be preserved with the ``{server}`` placeholder. However, using the ``{Server}`` placeholder is acceptable for retaining the parameter. 2. The developer intends **to remove an outdated parameter** after substituting a placeholder (refer to issue `952`_). To achieve this, identical names must be used, adhering to case-sensitive comparison rules. .. code-block:: json { "UpstreamPathTemplate": "/users?userId={userId}", "DownstreamPathTemplate": "/persons?personId={userId}" } | Thus, the ``{userId}`` placeholder and the ``userId`` parameter **have identical names**. As a result, the ``userId`` parameter is eliminated. | Be aware that, due to the case-sensitive nature of the comparison, the ``userId`` parameter will not be removed if the ``{userid}`` placeholder is used. .. _routing-upstream-host: Upstream Host [#f4]_ -------------------- This feature allows you to define routes based on the *upstream host*. It works by examining the ``Host`` header used by the client and incorporating it into the information used to identify a *route*. In order to use this feature, add the following to your configuration: .. code-block:: json { "UpstreamHost": "mydomain.com" } The *route* above will only match requests where the ``Host`` header value is ``mydomain.com``. If you do not set the ``UpstreamHost`` on a *route*, any ``Host`` header will match it. As a result, if you have two routes that are identical except for the ``UpstreamHost``, where one is null and the other is set, Ocelot will prioritize the one that is set. .. _routing-upstream-headers: Upstream Headers [#f5]_ ----------------------- In addition to routing by ``UpstreamPathTemplate``, you can also define ``UpstreamHeaderTemplates``. For a *route* to match, all headers specified in this dictionary object must be included in the request headers. .. code-block:: json :emphasize-lines: 3 { "UpstreamPathTemplate": "/", "UpstreamHeaderTemplates": { // dictionary "country": "uk", // 1st header "version": "v1" // 2nd header } } In this scenario, the *route* matches only if a request contains both headers with the specified values. Header placeholders ^^^^^^^^^^^^^^^^^^^ Let's explore a more interesting scenario where placeholders can be effectively utilized within your ``UpstreamHeaderTemplates``. Consider the following approach using the special placeholder format ``{header:placeholdername}``: .. code-block:: json { // downstream opts... "DownstreamPathTemplate": "/{versionnumber}/api", // with placeholder // upstream opts... "UpstreamHeaderTemplates": { "version": "{header:versionnumber}" // 'header:' prefix vs placeholder } } In this scenario, the entire value of the request header ``version`` is inserted into the ``DownstreamPathTemplate``. If needed, a more complex upstream header template can be specified using placeholders such as ``version-{header:version}_country-{header:country}``. **Note 1**: Placeholders are not required in the ``DownstreamPathTemplate``. This scenario can be used to enforce a specific header, regardless of its value. **Note 2**: Additionally, the ``UpstreamHeaderTemplates`` dictionary options are applicable for :doc:`../features/aggregation` as well. .. _routing-security-options: Security Options [#f6]_ ----------------------- Ocelot facilitates the management of multiple patterns for allowed and blocked IPs using the `IPAddressRange `_ package, which is licensed under the `MPL-2.0 license `_. This feature is designed to enhance IP management, allowing for the inclusion or exclusion of a broad IP range using CIDR notation or specific IP ranges. The current managed patterns are as follows: .. list-table:: :widths: 35 65 :header-rows: 1 * - *IP Rule* - *Example* * - Single IP - ``192.168.1.1`` * - IP Range - ``192.168.1.1-192.168.1.250`` * - IP Short Range - ``192.168.1.1-250`` * - IP Subnet - ``192.168.1.0/255.255.255.0`` * - CIDR IPv4 - ``192.168.1.0/24`` * - CIDR IPv6 - ``fe80::/10`` Here is a simple example: .. code-block:: json { "SecurityOptions": { "IPBlockedList": [ "192.168.0.0/23" ], "IPAllowedList": ["192.168.0.15", "192.168.1.15"], "ExcludeAllowedFromBlocked": true } } Please **note**: * The allowed/blocked lists are evaluated during configuration loading. * The ``ExcludeAllowedFromBlocked`` property enables specifying a wide range of blocked IP addresses while allowing a subrange of IP addresses. Default value: ``false``. * The absence of a property in *Security Options* is permitted, as it takes the default value. * *Security Options* can be configured *globally* in the ``GlobalConfiguration`` JSON [#f7]_. However, they are ignored if overriding options are specified at the route level. .. _routing-dynamic: Dynamic Routing [#f8]_ ---------------------- The concept of dynamic *routing* allows you to use a :doc:`../features/servicediscovery` provider, eliminating the need to manually configure *route* settings. For more details, refer to the :ref:`Dynamic Routing ` complete reference in the ":doc:`../features/servicediscovery`" chapter. Errors and Gotchas ------------------ .. _Show and tell: https://github.com/ThreeMammals/Ocelot/discussions/categories/show-and-tell .. _499 (Client Closed Request): https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.statuscodes.status499clientclosedrequest .. _503 (Service Unavailable): https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/503 In this section, Ocelot team has gathered user scenarios where routing behavior was unclear or errors appeared in the logs. Please note that the failed routing cases listed below do not represent all application configurations. If your case is not included, feel free to open a "`Show and tell`_" discussion. * **Magic 499 status**. According to Ocelot Core's design, HTTP status code `499 (Client Closed Request)`_ is returned in cases involving an ``OperationCanceledException``. Please note that due to extensive warning-level logging, you may encounter spikes in ``499`` responses—as discussed in thread `2072`_. This status is typically caused by: A) Forced cancellation of the request by the client B) Browser events such as page refreshes or closures while the downstream request is still in progress As a quick recipe, the Ocelot team recommends ensuring client stability and, if necessary, adjusting the :ref:`config-timeout` strategy: either increasing or decreasing the route :ref:`config-timeout` depending on your usage scenario and the behavior of the downstream service. * **Timeout errors aka 503 status**. According to Ocelot Core's design, HTTP status code `503 (Service Unavailable)`_ is returned in cases involving a ``TimeoutException``. This status is typically caused by: A) Slow downstream services that may fail to respond B) Large requests forwarded to slow downstream services As a quick recipe, the Ocelot team recommends increasing the route :ref:`config-timeout` in your configuration. This adjustment can help resolve timeout-related issues with sluggish downstream services, ultimately reducing occurrences of `503 (Service Unavailable)`_. .. _break: http://break.do **Note**: For comprehensive documentation regarding errors and status codes in Ocelot, please refer to the :doc:`../features/errorcodes` chapter. """" .. [#f1] The ":ref:`Embedded Placeholders `" feature was requested as part of issue `2199`_ , and released in version `23.4`_ .. [#f2] The ":ref:`Empty Placeholders `" feature is available starting in version `23.0`_, see issue `748`_ and the `23.0`_ release notes for details. .. [#f3] The ":ref:`Priority `" feature was requested as part of issue `270`_, and released in version `5.0.1`_ .. [#f4] The ":ref:`Upstream Host `" feature was requested as part of issue `209`_ (pull request `216`_), and released in version `3.0.1`_ .. [#f5] The ":ref:`Upstream Headers `" feature was proposed in issue `360`_ (pull request `1312`_), and released in version `23.3`_. .. [#f6] The ":ref:`Security Options `" feature was requested as part of issue `628`_ (version `12.0.1`_), then redesigned and improved by issue `1400`_ (version `23.4.1`_), and published in version `20.0`_ docs. .. [#f7] Global ":ref:`Security Options `" feature was requested as part of issue `2165`_ , and released in version `23.4.1`_. .. [#f8] The ":ref:`Dynamic Routing `" feature was requested as part of issue `340`_, and released in version `7.0.1`_. Refer to complete reference in the ":doc:`../features/servicediscovery`" chapter: :ref:`Dynamic Routing `. .. _model binding: https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-8.0#collections .. _Bind arrays and string values from headers and query strings: https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding?view=aspnetcore-8.0#bind-arrays-and-string-values-from-headers-and-query-strings .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _209: https://github.com/ThreeMammals/Ocelot/issues/209 .. _216: https://github.com/ThreeMammals/Ocelot/pull/216 .. _270: https://github.com/ThreeMammals/Ocelot/issues/270 .. _340: https://github.com/ThreeMammals/Ocelot/issues/340 .. _360: https://github.com/ThreeMammals/Ocelot/issues/360 .. _473: https://github.com/ThreeMammals/Ocelot/issues/473 .. _628: https://github.com/ThreeMammals/Ocelot/issues/628 .. _748: https://github.com/ThreeMammals/Ocelot/issues/748 .. _952: https://github.com/ThreeMammals/Ocelot/issues/952 .. _1174: https://github.com/ThreeMammals/Ocelot/issues/1174 .. _1312: https://github.com/ThreeMammals/Ocelot/pull/1312 .. _1400: https://github.com/ThreeMammals/Ocelot/issues/1400 .. _2072: https://github.com/ThreeMammals/Ocelot/discussions/2072 .. _2165: https://github.com/ThreeMammals/Ocelot/issues/2165 .. _2199: https://github.com/ThreeMammals/Ocelot/issues/2199 .. _3.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/3.0.1 .. _5.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/5.0.1 .. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1 .. _12.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/12.0.1 .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _23.4: https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.0 .. _23.4.1: https://github.com/ThreeMammals/Ocelot/releases/tag/23.4.1 ================================================ FILE: docs/features/servicediscovery.rst ================================================ Service Discovery ================= Ocelot allows you to specify a *service discovery* provider, which it uses to determine the host and port for the downstream service to which it forwards requests. Currently, this feature is only supported in the ``GlobalConfiguration`` section. This means the same *service discovery* provider is applied to all routes where a ``ServiceName`` is specified at the route level. .. _sd-consul: Consul ------ .. _Consul: https://www.consul.io/ .. _Ocelot.Provider.Consul: https://www.nuget.org/packages/Ocelot.Provider.Consul | Package: `Ocelot.Provider.Consul`_ | Namespace: ``Ocelot.Provider.Consul`` The first step is to install `the package `_, which adds `Consul`_ support to Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Consul To register *Consul* services, invoke the ``AddConsul()`` extension method using the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_. Include the following code in your `Program`_: .. code-block:: csharp using Ocelot.Provider.Consul; builder.Services .AddOcelot(builder.Configuration) .AddConsul(); // or .AddConsul() Currently, there are two types of *Consul* service discovery providers: ``Consul`` and ``PollConsul``. The default provider is ``Consul``. If the ``ConsulProviderFactory`` cannot read, understand, or parse the ``Type`` property of the ``ServiceProviderConfiguration`` object, a :ref:`sd-consul-provider` instance is created by the factory. Explore these types of *service discovery* providers and learn about their differences in the subsections: :ref:`sd-consul-provider` and :ref:`sd-pollconsul-provider`. **Note**: We have made the :ref:`sd-consul-provider` the default *service discovery* provider in Ocelot. .. _sd-consul-configuration-in-kv: Configuration in `KV Store`_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Add the following when registering your services. Ocelot will attempt to store and retrieve its :doc:`../features/configuration` in the *Consul* `KV Store`_: .. code-block:: csharp :emphasize-lines: 4 builder.Services .AddOcelot(builder.Configuration) .AddConsul() .AddConfigStoredInConsul(); You also need to add the following to your `ocelot.json`_ file. This allows Ocelot to locate your *Consul* agent and handle configuration loading and storage from *Consul*. .. code-block:: json "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 9500 } } The team decided to create this feature after working on the `Raft consensus `_ algorithm and realizing how challenging it was. Why not take advantage of the fact that `Consul`_ already provides this functionality? We believe this means that, to use Ocelot to its fullest potential, you currently need to adopt *Consul* as a dependency. **Note**: This feature has a `3-second TTL`_ cache before it makes a new request to your local *Consul* agent. .. _sd-consul-configuration-key: Configuration Key [#f2]_ ^^^^^^^^^^^^^^^^^^^^^^^^ If you are using *Consul* for :doc:`../features/configuration` (or other providers in the future), you may want to assign keys to your configurations. This allows you to manage multiple configurations. In order to specify the key, you need to set the ``ConfigurationKey`` property in the ``ServiceDiscoveryProvider`` options of the configuration JSON file. For example: .. code-block:: json :emphasize-lines: 5 "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 9500, "ConfigurationKey": "Ocelot_A" } } In this example, Ocelot will use ``Ocelot_A`` as the key for your configuration when looking it up in *Consul*. If you do not set the ``ConfigurationKey``, Ocelot will default to using the string ``InternalConfiguration`` as the key. .. _sd-consul-provider: ``Consul`` Provider ^^^^^^^^^^^^^^^^^^^ Class: `Ocelot.Provider.Consul.Consul `_ The following is required in the ``GlobalConfiguration`` section. The ``ServiceDiscoveryProvider`` property is mandatory. If you do not specify a host and port, the default `Consul`_ values will be used. **Note**: The ``Scheme`` option defaults to HTTP. This was introduced in pull request `1154`_ and defaults to ``http`` to avoid introducing a breaking change. .. code-block:: json :emphasize-lines: 5 "ServiceDiscoveryProvider": { "Scheme": "https", "Host": "localhost", "Port": 8500, "Type": "Consul" } In the future, we may add a feature that allows route-specific configuration. To instruct Ocelot that a route should use the *service discovery* provider for its host and port, you need to specify the ``ServiceName`` and the load balancer you wish to use for downstream requests. Currently, Ocelot supports the `RoundRobin `_ and `LeastConnection `_ algorithms. If no load balancer is specified, Ocelot will not perform load balancing for requests. .. code-block:: json { "ServiceName": "product", "LoadBalancerOptions": { "Type": "LeastConnection" } } When set up, Ocelot will look up the downstream host and port from the *service discovery* provider and balance requests across available services. .. _sd-pollconsul-provider: ``PollConsul`` Provider ^^^^^^^^^^^^^^^^^^^^^^^ Class: `Ocelot.Provider.Consul.PollConsul `_ A lot of users have requested a feature where Ocelot *polls Consul* for the latest service information instead of doing so per request. If you want Ocelot to *poll Consul* for the latest services, rather than relying on the default behavior (per request), you need to configure the following options: .. code-block:: json :emphasize-lines: 4-5 "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "PollConsul", "PollingInterval": 100 // ms } The polling interval, measured in milliseconds, specifies how frequently Ocelot calls `Consul`_ for service configuration updates. **Note**: There are trade-offs to consider. If you *poll Consul*, Ocelot may not detect if a service is down, depending on your polling interval. This could result in more errors compared to retrieving the latest services per request. The impact largely depends on the volatility of your services. For most users, this is unlikely to be a significant concern, and polling may offer a slight performance improvement over querying `Consul`_ per request (as a sidecar agent). However, if you are communicating with a remote `Consul`_ agent, polling provides a more noticeable performance improvement. Service Definition ^^^^^^^^^^^^^^^^^^ Your services need to be added to Consul in a manner similar to the example below (C# style, but hopefully it makes sense). The key point to note is to avoid including ``http`` or ``https`` in the ``Address`` field. We have received feedback regarding issues with the scheme being included in the ``Address``. After reviewing the "`Agents Overview `_" and "`Define services `_" documentation, we believe the **scheme** should not be included. In C# .. code-block:: csharp new AgentService() { ID = "some-id", Service = "some-service-name", Address = "localhost", Port = 8080, } Or, in JSON .. code-block:: json "Service": { "ID": "some-id", "Service": "some-service-name", "Address": "localhost", "Port": 8080 } ACL Token ^^^^^^^^^ If you are using `ACL `_ with *Consul*, Ocelot supports adding the ``X-Consul-Token`` header. To enable this functionality, you must add the following option: .. code-block:: json :emphasize-lines: 5 "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "Consul", "Token": "my-token" } Ocelot will add this token to the *Consul* client it uses for making requests, and this token will be applied to all subsequent requests. .. _sd-consul-service-builder: Consul Service Builder [#f3]_ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | Interface: ``IConsulServiceBuilder`` | Implementation: ``DefaultConsulServiceBuilder`` The Ocelot community has consistently reported issues with *Consul* services, both in the past and present, such as connectivity problems due to varying *Consul* agent definitions. Some DevOps engineers prefer grouping services as *Consul* `catalog nodes`_ by customizing the assignment of hostnames to node names, while others prioritize defining agent services using pure IP addresses as hosts, which is linked to the `954`_-bug dilemma. Since version `13.5.2`_, the process for constructing the downstream host and port in pull request `909`_ has been changed to prioritize the node name as the host over the agent service address IP. This may raise some criticism from the community. Version `23.3`_ introduced a customization feature that enables control over the service-building process through the ``DefaultConsulServiceBuilder`` class. This class includes virtual methods that developers and DevOps teams can override to suit their specific requirements. The current logic in the ``DefaultConsulServiceBuilder`` class is as follows: .. code-block:: csharp protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) => node != null ? node.Name : entry.Service.Address; Some DevOps engineers choose to disregard node names, opting for abstract identifiers instead of actual hostnames. However, our team strongly recommends assigning real hostnames or IP addresses to node names, considering this a best practice. If this approach does not align with your needs, or if you prefer not to invest time in detailing nodes for downstream services, you could define agent services without node names. In such cases, within a *Consul* setup, you would need to override the behavior of the ``DefaultConsulServiceBuilder`` class. For further information, refer to the ":ref:`sd-addconsul-generic-method`" section below. .. _sd-addconsul-generic-method: ``AddConsul`` method """"""""""""""""""""""" Signature: ``IOcelotBuilder AddConsul(this IOcelotBuilder builder)`` Overriding the ``DefaultConsulServiceBuilder`` behavior involves two steps: creating a new class that inherits from the ``IConsulServiceBuilder`` interface, and injecting this new behavior into the DI container using the ``AddConsul()`` helper. However, the fastest and most streamlined approach is to inherit directly from the ``DefaultConsulServiceBuilder`` class, as it provides greater flexibility. First, define a new service-building class: .. code-block:: csharp using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; public class MyConsulServiceBuilder : DefaultConsulServiceBuilder { public MyConsulServiceBuilder(IHttpContextAccessor contextAccessor, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) : base(contextAccessor, clientFactory, loggerFactory) { } // Use the agent service IP address as the downstream hostname protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; } Next, inject the new behavior into the DI container, as shown in the Ocelot-Consul setup: .. code-block:: csharp builder.Services .AddOcelot(builder.Configuration) .AddConsul(); Refer to the repository's `acceptance test`_ for further examples. .. _sd-eureka: Eureka [#f4]_ ------------- .. _Steeltoe: https://steeltoe.io .. _Pivotal: https://pivotal.io/platform .. _Eureka: https://www.nuget.org/packages/Steeltoe.Discovery.Eureka .. _Ocelot.Provider.Eureka: https://www.nuget.org/packages/Ocelot.Provider.Eureka | Package: `Ocelot.Provider.Eureka`_ | Namespace: ``Ocelot.Provider.Eureka`` This feature supports the Netflix `Eureka`_ *service discovery* provider. The primary reason for this is that it is a key product of `Steeltoe`_, which is associated with `Pivotal`_. Now, enough of the background! The first step is to install `the package `__ that provides `Eureka`_ support for Ocelot: .. code-block:: powershell Install-Package Ocelot.Provider.Eureka Next, add the following to your `Program `__: .. code-block:: csharp using Ocelot.Provider.Eureka; builder.Services .AddOcelot(builder.Configuration) .AddEureka(); Finally, to enable this setup, include the following in your `ocelot.json `__ file: .. code-block:: json "ServiceDiscoveryProvider": { "Type": "Eureka" } Following the guide `here `_, you may also need to add some configurations to `appsettings.json `_. For example, the JSON below informs the `Steeltoe`_ / `Pivotal`_ services where to locate the service discovery server and whether the service should register with it: .. code-block:: json "eureka": { "client": { "serviceUrl": "http://localhost:8761/eureka/", "shouldRegisterWithEureka": false, "shouldFetchRegistry": true } } If ``shouldRegisterWithEureka`` is set to ``false``, ``shouldFetchRegistry`` will default to ``true``, so you do not need to set it explicitly; however, it has been included here for clarity. Ocelot will now register all necessary services during startup and, if the JSON above is provided, it will register itself with *Eureka*. One of the services polls *Eureka* every 30 seconds (default) to retrieve the latest service state and persists this information in memory. When Ocelot requests a given service, it retrieves the data from memory, minimizing performance issues. If not explicitly specified in `ocelot.json `__, Ocelot will use the scheme (``http``, ``https``) set in *Eureka*. .. _sd-service-fabric: Service Fabric -------------- .. _Service Fabric: https://azure.microsoft.com/en-us/products/service-fabric/ .. _Microsoft.ServiceFabric: https://www.nuget.org/packages/Microsoft.ServiceFabric If you have services deployed in Azure `Service Fabric`_, you typically use the naming service to access them. Please refer to the :doc:`../features/servicefabric` chapter for the complete *essential* documentation. **Note**: Currently, the ``ServiceFabric`` *service discovery* provider is tightly coupled with Ocelot core interfaces, making it a part of Ocelot Core and implemented as the ``ServiceFabricServiceDiscoveryProvider`` class. At present, there is no Ocelot extension package that integrates with the `Microsoft.ServiceFabric`_ package or any other relevant package. However, the Ocelot team plans to address this in future development, as we believe `Service Fabric`_ is an essential and popular product in the .NET and Azure development world. If anyone in the Ocelot community is a professional Azure developer with extensive `Service Fabric`_ experience, please contact our development team directly via GitHub or email. .. _sd-dynamic-routing: Dynamic Routing [#f5]_ ---------------------- The idea is to enable *dynamic routing* mode when using a *service discovery* provider. In this mode, Ocelot uses the first segment of the upstream path to look up the downstream service via the *service discovery* provider. An example of this would be calling Ocelot with a URL like * ``https://api.ocelot.net/product/products`` Ocelot will take the first segment of the path, which is ``product``, and use it as a key to look up the service in :ref:`sd-consul`. If :ref:`sd-consul-provider` returns a service, Ocelot will request it using the host and port provided by `Consul`_, appending the remaining path segments—in this case, ``products``—to construct final downstream URL: * ``http://hostfromconsul:portfromconsul/products`` Ocelot will append any query string to the downstream URL as usual. .. warning:: To enable *dynamic routing*, the `ocelot.json`_ configuration must contain no static routes in the ``Routes`` collection! Currently, dynamic routes and static routes cannot be mixed. Additionally, you need to specify the details of the *service discovery* provider as outlined above, along with the downstream ``http(s)`` scheme under ``DownstreamScheme``. .. note:: In addition to the global ``ServiceDiscoveryProvider`` section, the :ref:`config-global-configuration-schema` includes configurable options such as ``DownstreamScheme``, ``CacheOptions``, ``HttpHandlerOptions``, ``LoadBalancerOptions``, ``QoSOptions``, ``RateLimitOptions``, and ``Timeout``. These options are applicable to all dynamic routes, globally. Moreover, starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports these options for overriding global settings. For instance, when exposing Ocelot publicly over HTTPS while routing to internal services over HTTP, your configuration may resemble the following: .. code-block:: json { "Routes": [], // must be empty to enable dynamic routing! "DynamicRoutes": [ // overriding goes here ], "GlobalConfiguration": { "BaseUrl": "https://api.ocelot.net", "DownstreamScheme": "http", // default scheme for all internal services, no SSL "ServiceDiscoveryProvider": { "Host": "localhost", // if Consul is hosted on the same machine as Ocelot "Port": 8500, "Type": "Consul", "Namespace": "" // not supported for Consul, but supported for Kubernetes }, "CacheOptions": { "TtlSeconds": 300 // 5 minutes }, "HttpHandlerOptions": { "AllowAutoRedirect": false, "UseCookieContainer": false, "UseTracing": false }, "LoadBalancerOptions": { "Type": "LeastConnection" }, "QoSOptions": { "MinimumThroughput": 2, "BreakDuration": 333, "Timeout": 3000 // ms }, "RateLimitOptions": { "ClientIdHeader": "Oc-DynamicRouting-Client", "QuotaMessage": "No Quota!", "StatusCode": 499 // special shared status } } } .. _sd-dynamic-routing-configuration: Configuration [#f6]_ ^^^^^^^^^^^^^^^^^^^^ Ocelot also allows configuration of a ``DynamicRoutes`` collection consisting of :ref:`config-dynamic-route-schema` objects. This enables overriding ``RateLimitOptions`` for each downstream service, along with other schema-level overrides. Dynamic route options are particularly useful when there are multiple services—such as a '``product``' service and a '``search``' service—and stricter rate limits need to be applied to one over the other. The final configuration looks like: .. code-block:: json { "DynamicRoutes": [ { "ServiceName": "product", "ServiceNamespace": "", // not supported for Consul, but supported for Kubernetes "RateLimitOptions": { "Limit": 5, "Period": "1s", "Wait": "1.5s" // hybrid fixed window } }, { "ServiceName": "notification", "CacheOptions": { "TtlSeconds": 0 // disable cache for notifying }, "HttpHandlerOptions": { "UseTracing": false // disable tracing }, "LoadBalancerOptions": { "Type": "LeastConnection" // switch from RoundRobin to LeastConnection }, "RateLimitOptions": { "EnableRateLimiting": false // notification service is unlimited! } } ], "GlobalConfiguration": { "BaseUrl": "https://api.ocelot.net", "DownstreamScheme": "http", "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 8500, "Type": "Consul", "Namespace": "" // not supported for Consul, but supported for Kubernetes }, "CacheOptions": { "TtlSeconds": 300 // 5 minutes }, "HttpHandlerOptions": { "PooledConnectionLifetimeSeconds": 600, // change the default value from 2 minutes to 10 minutes "UseTracing": true // enable tracing globally }, "LoadBalancerOptions": { "Type": "RoundRobin" }, "RateLimitOptions": { "ClientIdHeader": "Oc-DynamicRouting-Client", "ClientWhitelist": ["ocelot-client1-preshared-key"], "Limit": 5, "Period": "10s", // fixed window "QuotaMessage": "No Quota!", "StatusCode": 499 // special shared status } } } This configuration means that when a request is sent to Ocelot at ``/product/*``, *dynamic routing* is activated, and Ocelot applies the rate limiting rules defined for the '``product``' service in the ``DynamicRoutes`` section, as described in the :doc:`../features/ratelimiting` documentation. The '``notification``' service is unlimited because both caching, tracing, and rate limiting are disabled. All other services use the global ``RateLimitOptions`` along with the other specified options. .. warning:: Dynamic route ``RateLimitRule`` option is deprecated! The `old schema `_ ``RateLimitRule`` section is deprecated in version `24.1`_! Use ``RateLimitOptions`` instead of ``RateLimitRule``! Note that ``RateLimitRule`` will be removed in version `25.0`_! For backward compatibility in version `24.1`_, the ``RateLimitRule`` section takes precedence over the ``RateLimitOptions`` section. .. note:: The ``ServiceNamespace`` option was introduced in version `24.1`_ to enable precise overrides for the :doc:`../features/kubernetes` providers. If ``ServiceNamespace`` is left empty or undefined, only **one** dynamic route with the same ``ServiceName`` may be defined in the ``DynamicRoutes`` collection. .. _sd-custom-providers: Custom Providers ---------------- Ocelot also enables you to create a custom *Service Discovery* implementation by implementing the ``IServiceDiscoveryProvider`` interface, as demonstrated in the following example: .. code-block:: csharp public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider { private readonly IServiceProvider _serviceProvider; private readonly ServiceProviderConfiguration _config; private readonly DownstreamRoute _downstreamRoute; public MyServiceDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute) { _serviceProvider = serviceProvider; _config = config; _downstreamRoute = downstreamRoute; } public Task> GetAsync() { var services = new List(); // ... // Add service(s) to the list matching the _downstreamRoute return services; } } And set its class name as the provider type in `ocelot.json`_: .. code-block:: json "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Type": "MyServiceDiscoveryProvider" } } Finally, in the `Program`_, register a ``ServiceDiscoveryFinderDelegate`` to initialize and return the provider: .. code-block:: csharp ServiceDiscoveryFinderDelegate serviceDiscoveryFinder = (provider, config, route) => new MyServiceDiscoveryProvider(provider, config, route); builder.Services .AddSingleton(serviceDiscoveryFinder) .AddOcelot(builder.Configuration); .. _sd-sample: Sample ------ To offer a basic template for a :ref:`sd-custom-providers`, we have created a sample: | Project: `samples `_ / `ServiceDiscovery `_ | Solution: `Ocelot.Samples.ServiceDiscovery.sln `_ This solution includes the following projects: - :ref:`sd-api-gateway` - :ref:`sd-downstream-service` The solution is ready for deployment. All services are fully configured, with ports and hosts prepared for immediate use (when running in Visual Studio). Complete instructions for running this solution can be found in the `README.md `_ file. .. _sd-downstream-service: DownstreamService ^^^^^^^^^^^^^^^^^ This project provides a single downstream service that can be reused across :ref:`sd-api-gateway` routes. It includes multiple ``launchSettings.json`` profiles to support your preferred launch and hosting scenarios, such as Visual Studio sessions, Kestrel console hosting, and Docker deployments. .. _sd-api-gateway: ApiGateway ^^^^^^^^^^ This project includes a custom *Service Discovery* provider and contains only route(s) to :ref:`sd-downstream-service` services in the `ocelot.json`_ file. You are free to add more routes! The main source code for the custom provider is located in the `ServiceDiscovery `__ folder, specifically in the ``MyServiceDiscoveryProvider`` and ``MyServiceDiscoveryProviderFactory`` classes. Feel free to design and develop these classes to suit your needs! Additionally, the cornerstone of this custom provider is the `Program`_ code, where you can select from simple or more complex design and implementation options: .. code-block:: csharp // Perform initialization from application configuration or hardcode/choose the best option. bool easyWay = true; if (easyWay) { // Design #1: Define a custom finder delegate to instantiate a custom provider // under the default factory (ServiceDiscoveryProviderFactory). builder.Services .AddSingleton((serviceProvider, config, downstreamRoute) => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute)); } else { // Design #2: Abstract from the default factory (ServiceDiscoveryProviderFactory) and FinderDelegate, // and create your own factory by implementing the IServiceDiscoveryProviderFactory interface. builder.Services .RemoveAll() .AddSingleton(); // This will not be called but is required for internal validators. It's also a handy workaround. builder.Services .AddSingleton((serviceProvider, config, downstreamRoute) => null); } builder.Services .AddOcelot(builder.Configuration); The "easy way" (lite design #1) involves designing only the provider class and specifying the ``ServiceDiscoveryFinderDelegate`` object for the default ``ServiceDiscoveryProviderFactory`` in the Ocelot core. A more complex design #2 involves developing both the provider and provider factory classes. Once this is done, you need to add the ``IServiceDiscoveryProviderFactory`` interface to the DI container and remove the default ``ServiceDiscoveryProviderFactory`` class. Note that in this case, the Ocelot core will not use the ``ServiceDiscoveryProviderFactory`` by default. Additionally, you do not need to specify ``"Type": "MyServiceDiscoveryProvider"`` in the ``ServiceDiscoveryProvider`` global options. However, you can retain this ``Type`` option to maintain compatibility between both designs. """" .. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. .. [#f2] The ":ref:`Configuration Key `" feature was requested in issue `346`_ and introduced in version `7.0.0`_. .. [#f3] The customization of ":ref:`Consul Service Builder `" was implemented as part of bug fix `954`_, and the feature was delivered in version `23.3`_. .. [#f4] The :ref:`Eureka ` feature, requested in issue `262`_ to add support for the Netflix `Eureka`_ *service discovery* provider, was released in version `5.5.4`_. .. [#f5] The ":ref:`Dynamic Routing `" feature was requested in issue `340`_ (pull request `351`_) and released in version `7.0.1`_. Later, the new ``DynamicRoutes`` :doc:`../features/configuration` section was introduced in pull request `508`_ and released in version `8.0.4`_. .. [#f6] The :ref:`Configuration ` feature of :ref:`Dynamic Routing ` was requested in issue `585`_, then significantly redeveloped and released in version `24.1`_. .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/ApiGateway/ocelot.json .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceDiscovery/ApiGateway/Program.cs .. _KV Store: https://developer.hashicorp.com/consul/docs/dynamic-app-config/kv .. _3-second TTL: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+TimeSpan.FromSeconds%283%29&type=code .. _catalog nodes: https://developer.hashicorp.com/consul/api-docs/catalog#list-nodes .. _acceptance test: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+ShouldReturnServiceAddressByOverriddenServiceBuilderWhenThereIsANode+WithConsulServiceBuilder&type=code .. _262: https://github.com/ThreeMammals/Ocelot/issues/262 .. _340: https://github.com/ThreeMammals/Ocelot/issues/340 .. _346: https://github.com/ThreeMammals/Ocelot/issues/346 .. _351: https://github.com/ThreeMammals/Ocelot/pull/351 .. _508: https://github.com/ThreeMammals/Ocelot/pull/508 .. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. _909: https://github.com/ThreeMammals/Ocelot/pull/909 .. _954: https://github.com/ThreeMammals/Ocelot/issues/954 .. _1154: https://github.com/ThreeMammals/Ocelot/pull/1154 .. _5.5.4: https://github.com/ThreeMammals/Ocelot/releases/tag/5.5.4 .. _7.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.0 .. _7.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/7.0.1 .. _8.0.4: https://github.com/ThreeMammals/Ocelot/releases/tag/8.0.4 .. _13.5.2: https://github.com/ThreeMammals/Ocelot/releases/tag/13.5.2 .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/milestone/13 ================================================ FILE: docs/features/servicefabric.rst ================================================ Service Fabric ============== [#f1]_ Feature of: :doc:`../features/servicediscovery` If you have services deployed in Azure `Service Fabric`_ you will normally use the naming service to access them. This feature allows to set up a route that will work in `Service Fabric`_. Configuration ------------- The most important thing is the ``ServiceName``, which is composed of the `Service Fabric`_ application name followed by the specific service name. Additionally, the ``ServiceDiscoveryProvider`` needs to be configured in ``GlobalConfiguration``. The example below demonstrates a typical configuration. It assumes that *Service Fabric* is running on ``localhost`` and that the naming service is using port ``19081``. The example below is taken from the :ref:`sf-sample`, so please check it if this doesn't make sense! .. code-block:: json :emphasize-lines: 8, 17 { "Routes": [ { "DownstreamScheme": "http", "DownstreamPathTemplate": "/api/values", "UpstreamPathTemplate": "/EquipmentInterfaces", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "OcelotServiceApplication/OcelotApplicationService" } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.net", "RequestIdKey": "Oc-RequestId", "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 19081, "Type": "ServiceFabric" } } } If you are using stateless or guest exe services, Ocelot can proxy through the naming service without requiring additional configuration. However, if you are using stateful or actor services, you must include the ``PartitionKind`` and ``PartitionKey`` query string values in the client request, e.g., GET ``http://ocelot.com/EquipmentInterfaces?PartitionKind=xxx&PartitionKey=xxx`` There is no way for Ocelot to determine these values automatically. .. _sf-placeholders: Placeholders [#f2]_ ------------------- In Ocelot, *placeholders* for variables can be inserted into the ``UpstreamPathTemplate`` and ``ServiceName`` using the format ``{something}``. **Note**: The *placeholder* variable must exist in both the ``DownstreamPathTemplate`` (or ``ServiceName``) and the ``UpstreamPathTemplate``. Specifically, the ``UpstreamPathTemplate`` must include all *placeholders* found in the ``DownstreamPathTemplate`` and ``ServiceName``. Failure to meet this requirement will prevent Ocelot from starting due to validation errors, which are logged. Once the validation stage is completed, Ocelot replaces the placeholder values in the ``UpstreamPathTemplate`` with those from the ``DownstreamPathTemplate`` and/or ``ServiceName`` for each processed request. Thus, the *Service Fabric* :ref:`sf-placeholders` feature operates similarly to the original routing :ref:`routing-placeholders` feature but includes the ``ServiceName`` property in its processing. | Here is an example of the ``version`` variable in the *Service Fabric* service name. | Given the following `ocelot.json`_: .. code-block:: json :emphasize-lines: 6, 14 { "Routes": [ { "UpstreamPathTemplate": "/api/{version}/{endpoint}", "DownstreamPathTemplate": "/{endpoint}", "ServiceName": "Service_{version}/Api", } ], "GlobalConfiguration": { "BaseUrl": "https://ocelot.com", "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 19081, "Type": "ServiceFabric" } } } When you make Ocelot request: * ``GET https://ocelot.com/api/1.0/products`` The *Service Fabric* request will be: * ``GET http://localhost:19081/Service_1.0/Api/products`` .. _sf-sample: Sample ------ In order to introduce the *Service Fabric* feature, we have prepared a sample: | Project: `samples `_ / `ServiceFabric `_ | Solution: `Ocelot.Samples.ServiceFabric.sln `_ This solution includes the following projects: - ``Ocelot.Samples.ServiceFabric.ApiGateway.csproj`` - ``Ocelot.Samples.ServiceFabric.DownstreamService.csproj`` Complete instructions for running this solution can be found in the `README.md `_ file. .. note:: Please consider this solution as a demonstration of integration; it is outdated as of 2025. Therefore, this solution is a draft and requires further development for practical usage and deployment in the Azure cloud. Additionally, refer to the team's notes in the :ref:`sd-service-fabric` section! """" .. [#f1] Historically, the "`Service Fabric <#service-fabric>`__" feature is one of Ocelot's earliest and foundational features, first requested in issue `238`_. It was initially released in version `3.1.9`_. .. [#f2] The ":ref:`Placeholders `" feature was requested in issue `721`_ and implemented by pull request `722`_ as part of version `13.0.0`_. .. _Service Fabric: https://azure.microsoft.com/en-us/products/service-fabric/ .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/ServiceFabric/ApiGateway/ocelot.json .. _238: https://github.com/ThreeMammals/Ocelot/issues/238 .. _721: https://github.com/ThreeMammals/Ocelot/issues/721 .. _722: https://github.com/ThreeMammals/Ocelot/pull/722 .. _3.1.9: https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.9 .. _13.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/13.0.0 ================================================ FILE: docs/features/tracing.rst ================================================ Tracing ======= Feature of: :doc:`../features/logging` * `.NET logging and tracing | .NET | Microsoft Learn `_ * `.NET distributed tracing | .NET | Microsoft Learn `_ This chapter explains how to perform distributed tracing using Ocelot. .. |opentracing-csharp Logo| image:: https://avatars.githubusercontent.com/u/15482765 :alt: opentracing-csharp Logo :width: 30 |opentracing-csharp Logo| OpenTracing ------------------------------------- .. _OpenTracing: https://opentracing.io | Package: `Ocelot.Tracing.OpenTracing `_ | Namespace: ``Ocelot.Tracing.OpenTracing`` Ocelot provides tracing functionality through the excellent project from `opentracing-csharp `_ repository. The code for Ocelot integration can be found in this `Ocelot project `_. The example below uses the `C# Client for Jaeger `_ to provide the tracer used in Ocelot. To add `OpenTracing`_ services, you must call the ``AddOpenTracing()`` extension method on the ``OcelotBuilder`` returned by ``AddOcelot()`` [#f1]_, as shown below: .. code-block:: csharp :emphasize-lines: 11 builder.Services .AddSingleton(serviceProvider => { var loggerFactory = serviceProvider.GetService(); var config = new Jaeger.Configuration(builder.Environment.ApplicationName, loggerFactory); var tracer = config.GetTracer(); GlobalTracer.Register(tracer); return tracer; }) .AddOcelot(builder.Configuration) .AddOpenTracing(); Then, in your `ocelot.json `__, add the following to the route you want to trace: .. code-block:: json "HttpHandlerOptions": { "UseTracing": true } Ocelot will now send tracing information to `Jaeger `_ whenever this route is called. **Note 1**: A clean yet functional sample can be found here: `Ocelot.Samples.OpenTracing `_. **Note 2**: The `OpenTracing`_ project was archived on January 31, 2022 (see `the article `_). The Ocelot team is planning to decide on a migration to `OpenTelemetry `_, which is highly desirable. .. _tr-butterfly: Butterfly --------- .. _Butterfly: https://github.com/liuhaoyang/butterfly | Package: `Ocelot.Tracing.Butterfly `_ | Namespace: ``Ocelot.Tracing.Butterfly`` Ocelot provides tracing functionality through the excellent `Butterfly`_ project. The code for the Ocelot integration can be found in this `Ocelot project `__. To use the tracing functionality, please refer to the `Butterfly`_ documentation. In Ocelot, you need to add the NuGet package if you wish to trace a route: .. code-block:: powershell Install-Package Ocelot.Tracing.Butterfly In your `Program`_, to add `Butterfly`_ services, you must call the ``AddButterfly()`` extension method on the ``OcelotBuilder`` returned by ``AddOcelot()``, as shown below: .. code-block:: csharp :emphasize-lines: 5 using Ocelot.Tracing.Butterfly; builder.Services .AddOcelot(builder.Configuration) .AddButterfly(options => { // This is the URL that the Butterfly collector server is running on... options.CollectorUrl = "http://localhost:9618"; options.Service = "Ocelot"; }); Then, in your `ocelot.json`_, add the following to the route you want to trace: .. code-block:: json "HttpHandlerOptions": { "UseTracing": true } Ocelot will now send tracing information to `Butterfly`_ whenever this route is called. **Note**: The `Butterfly`_ project has not been supported for more than seven years, as of 2025. The latest release of the `Butterfly.Client `_ package (version `0.0.8 `_) was made on February 22, 2018. The Ocelot team is planning to discontinue the `Ocelot.Tracing.Butterfly`_ package, which is scheduled to happen after the release of Ocelot version `24.1`_. """" .. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 ================================================ FILE: docs/features/websockets.rst ================================================ Websockets ========== * `WebSockets Standard `_ by WHATWG organization * `The WebSocket Protocol `_ by Internet Engineering Task Force (IETF) organization Ocelot supports proxying `WebSockets `_ [#f1]_ with some extra bits. Configuration ------------- To enable *WebSockets* proxying with Ocelot, you need to do the following in your `Program`_: .. code-block:: csharp :emphasize-lines: 2 var app = builder.Build(); app.UseWebSockets(); await app.UseOcelot(); await app.RunAsync(); Then, in your `ocelot.json`_, add the following to proxy a route using *WebSockets*: .. code-block:: json :emphasize-lines: 4 { "UpstreamPathTemplate": "/", "DownstreamPathTemplate": "/ws", "DownstreamScheme": "ws", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ] } With this configuration, Ocelot will match any *WebSockets* traffic that comes in on / and proxy it to ``localhost:5001/ws``. For clarity, Ocelot will receive messages from the upstream client, proxy them to the downstream service, receive messages from the downstream service, and then proxy them back to the upstream client. Handy Links ----------- * WHATWG: `WebSockets Standard `_ * Mozilla Developer Network: `The WebSocket API (WebSockets) `_ * Microsoft Learn: `WebSockets support in ASP.NET Core `_ * Microsoft Learn: `WebSockets support in .NET `_ .. _ws-signalr: SignalR [#f2]_ -------------- Welcome to `Real-time ASP.NET with SignalR `_ Ocelot supports proxying *SignalR*. To enable this with Ocelot, you need to do the following: First, install the `SignalR Client `_ NuGet package: .. code-block:: powershell Install-Package Microsoft.AspNetCore.SignalR.Client .. _break: http://break.do **Note**: SignalR is `part of the ASP.NET Core `_ and can be referenced as follows: .. code-block:: xml More information on framework compatibility can be found in the instructions: `Use ASP.NET Core APIs in a class library `_. Second, you need to configure your application to use *SignalR*. A complete reference can be found here: `ASP.NET Core SignalR configuration `_. .. code-block:: csharp builder.Services.AddOcelot(builder.Configuration); builder.Services.AddSignalR(); .. _break2: http://break.do **Note**: Make sure to pay attention to the transport-level configuration for *WebSockets*. Ensure that allowed transports are properly configured to enable *WebSockets* connections: `ASP.NET Core SignalR configuration `_. Next, include the following in your `ocelot.json`_ file to proxy a route using *SignalR*. Note that standard Ocelot routing rules apply; the key aspect is that the scheme is set to ``ws`` (*WebSockets*). .. code-block:: json :emphasize-lines: 4 { "UpstreamPathTemplate": "/gateway/{catchAll}", "DownstreamPathTemplate": "/{catchAll}", "DownstreamScheme": "ws", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ] } .. _ws-secure: WebSocket Secure ---------------- If you define a route with the *secured WebSockets* protocol, use the ``wss`` scheme: .. code-block:: json "DownstreamScheme": "wss", Keep in mind that you can use WebSocket SSL for both :ref:`SignalR ` and :doc:`../features/websockets`. **Note**: To understand ``wss`` scheme, browse to this documentation: * IETF | The WebSocket Protocol: `WebSocket URIs `_ * Microsoft Learn: `Secure your connection with TLS/SSL `_ * Microsoft Learn: `Search for "secure websocket" `_ If you want to ignore SSL warnings (errors) [#f3]_, configure your route as follows: .. code-block:: json "DownstreamScheme": "wss", "DangerousAcceptAnyServerCertificateValidator": true, *However, we strongly advise against this!* Refer to the official notes regarding :ref:`ssl-errors` in the :doc:`../features/configuration` documentation. There, you can also explore best practices tailored for your environments. Supported --------- 1. :doc:`../features/routing` 2. :doc:`../features/loadbalancer` 3. :doc:`../features/servicediscovery` This means you can configure your downstream services to run *WebSockets* and either: * Include multiple ``DownstreamHostAndPorts`` in your route configuration. * Connect your route to a :doc:`../features/servicediscovery` provider. This allows you to load balance requests, which we think is pretty cool! Not Supported ------------- Unfortunately, many Ocelot features are not specific to *WebSockets*, such as header handling and HTTP client functionalities. Below is a list of features that will not work: 1. :doc:`../features/tracing` 2. :doc:`../features/logging` :ref:`lg-request-id` 3. :doc:`../features/aggregation` 4. :doc:`../features/ratelimiting` 5. :doc:`../features/qualityofservice` 6. :doc:`../features/middlewareinjection` 7. :doc:`../features/headerstransformation` 8. :doc:`../features/delegatinghandlers` 9. :doc:`../features/claimstransformation` 10. :doc:`../features/caching` 11. :doc:`../features/authentication` [#f4]_ 12. :doc:`../features/authorization` We cannot be entirely sure how this feature will behave once it is widely used. Therefore, thorough testing is strongly recommended! Roadmap ------- *WebSockets* and *SignalR* are being actively developed by the .NET community. It is important to stay updated with trends and regularly check for new releases in the official documentation: * `WebSockets docs `_ * `SignalR docs `_ As a team, we are unable to provide direct development advice. However, feel free to ask questions or explore coding recipes in `Discussions `_ of the repository. Additionally, we welcome any bug reports, enhancement suggestions, or proposals related to this feature. |octocat| .. note:: The Ocelot team considers the current implementation of the *WebSockets* feature to be obsolete, as it is based on the `WebSocketsProxyMiddleware `_ class. *WebSockets* are a part of the ASP.NET Core framework, which includes the native `WebSocketMiddleware `_ class. We have a strong intention to either migrate or redesign this feature. For more details, see issue `1707`_. """" .. [#f1] The :doc:`../features/websockets` functionality was requested in issue `212 `_ and introduced in version `5.3.0`_. .. [#f2] The :ref:`SignalR ` functionality was requested in issue `344`_ and published in version `8.0.7`_. .. [#f3] The ":ref:`ws-secure`" feature includes a ``wss`` scheme fake validator, which was introduced in pull request `1377`_ as part of issues `1375`_, `1237`_, and others. This "life hack" for self-signed SSL certificates is available starting from version `20.0`_. However, it will be either removed or reworked in future releases. For further details, refer to the :ref:`ssl-errors` section. .. [#f4] If requested, we might explore options for implementing basic authentication. .. _Program: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/Program.cs .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _212: https://github.com/ThreeMammals/Ocelot/issues/212 .. _344: https://github.com/ThreeMammals/Ocelot/issues/344 .. _1237: https://github.com/ThreeMammals/Ocelot/issues/1237 .. _1375: https://github.com/ThreeMammals/Ocelot/issues/1375 .. _1377: https://github.com/ThreeMammals/Ocelot/pull/1377 .. _1707: https://github.com/ThreeMammals/Ocelot/issues/1707 .. _5.3.0: https://github.com/ThreeMammals/Ocelot/releases/tag/5.3.0 .. _8.0.7: https://github.com/ThreeMammals/Ocelot/releases/tag/8.0.7 .. _20.0: https://github.com/ThreeMammals/Ocelot/releases/tag/20.0.0 .. |octocat| image:: https://github.githubassets.com/images/icons/emoji/octocat.png :alt: octocat :height: 25 :class: img-valign-middle ================================================ FILE: docs/index.rst ================================================ .. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0 .. role:: htm(raw) :format: html .. role:: pdf(raw) :format: latex pdflatex ############## Ocelot `25.0`_ ############## Thanks for taking a look at the Ocelot documentation! Please use the left hand **Navigation** sidebar to get around, or see the :htm:`Table of Contents below.` :pdf:`\textbf{Table of Contents} above.` The team recommends that newcomers to Ocelot's world start with the ":doc:`Introduction <../introduction/bigpicture>`" chapters. For seasoned fans of Ocelot with a Production environment, it is advised to always consult the :ref:`release-notes` in the :doc:`../releasenotes` section before upgrading the app to the latest `25.0`_ version. All **Features** are listed in alphabetical order. The primary features include :doc:`../features/configuration` and :doc:`../features/routing`. Additional tips for building Ocelot can be found in the ":doc:`Building Ocelot <../building/building>`" section. We adhere to a :doc:`../building/devprocess` which is a part of :doc:`../building/releaseprocess`. :htm:`

Table of Contents

` .. toctree:: :maxdepth: 2 :caption: Welcome releasenotes .. toctree:: :maxdepth: 3 :caption: Introduction introduction/bigpicture introduction/gettingstarted introduction/notsupported introduction/gotchas .. toctree:: :maxdepth: 3 :caption: Features features/administration features/aggregation features/authentication features/authorization features/caching features/claimstransformation features/configuration features/delegatinghandlers features/dependencyinjection features/errorcodes features/graphql features/headerstransformation features/kubernetes features/loadbalancer features/logging features/metadata features/methodtransformation features/middlewareinjection features/qualityofservice features/ratelimiting features/routing features/servicediscovery features/servicefabric features/tracing features/websockets .. toctree:: :maxdepth: 3 :caption: Building Ocelot building/building building/devprocess building/releaseprocess :htm:`
` ================================================ FILE: docs/introduction/bigpicture.rst ================================================ Big Picture =========== Ocelot is aimed at people using .NET running a microservices (service-oriented) architecture (aka SOA) that need a unified point of entry into their system. However it will work with anything that speaks HTTP(S) and run on any platform that `ASP.NET Core `_ supports. .. TODO Decide what to do with this paragraph?.. In particular we want easy integration with `IdentityServer `_ reference and `Bearer `_ tokens. We have been unable to find this in our current workplace without having to write our own Javascript middlewares to handle the IdentityServer reference tokens. We would rather use the IdentityServer code that already exists to do this. Ocelot consists of a series of ASP.NET Core `middlewares `_ arranged in a specific order. Ocelot manipulates the ``HttpRequest`` object into a state specified by its configuration until it reaches a request builder middleware, where it creates a ``HttpRequestMessage`` object which is used to make a request to a downstream service. The middleware that makes the request is the last thing in the Ocelot pipeline. It does not call the next middleware. The response from the downstream service is retrieved as the request goes back up the Ocelot pipeline. There is a piece of middleware that maps the ``HttpResponseMessage`` onto the ``HttpResponse`` object, and that is returned to the client. That is basically it with a bunch of other features! The following are configurations that you use when deploying Ocelot. Basic Implementation -------------------- .. image:: ../images/OcelotBasic.jpg .. TODO Do not advertise the product because of non-OSS status With IdentityServer ^^^^^^^^^^^^^^^^^^^ .. image:: ../images/OcelotIndentityServer.jpg Multiple Instances ------------------ .. image:: ../images/OcelotMultipleInstances.jpg With Consul ----------- .. image:: ../images/OcelotMultipleInstancesConsul.jpg With Service Fabric ------------------- .. image:: ../images/OcelotServiceFabric.jpg ================================================ FILE: docs/introduction/gettingstarted.rst ================================================ Getting Started =============== Ocelot is designed to work with `ASP.NET Core `_ and is currently on `.NET 8 `_ `LTS `_ and `.NET 9 `_ `STS `_ frameworks. Install ------- Install Ocelot and it's dependencies using `NuGet `_. You will need to create a `ASP.NET Core minimal API project `_ with "ASP.NET Core Empty" template but without ``app.Map*`` methods, and bring the package into it. Then follow the startup below and :doc:`../features/configuration` sections to get up and running. .. code-block:: powershell Install-Package Ocelot All versions can be found in the `NuGet Gallery | Ocelot `_. .. _getstarted-configuration: Configuration ------------- The following is a very basic `ocelot.json`_. It won't do anything but should get Ocelot starting. .. code-block:: json { "Aggregates": [], // optional "Routes": [], // required section "DynamicRoutes": [], // optional section "GlobalConfiguration": { // required "BaseUrl": "https://api.mybusiness.com" } } If you want some example that actually does something use the following: .. code-block:: json { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/posts/{id}", "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ] } ], "GlobalConfiguration": { "BaseUrl": "https://api.mybusiness.com" } } The most important thing to note here is ``BaseUrl`` property. Ocelot needs to know the URL it is running under in order to do Header :ref:`ht-find-and-replace` and for certain :doc:`../features/administration` configurations. When setting this URL it should be the external URL that clients will see Ocelot running on e.g. If you are running containers Ocelot might run on the URL ``http://123.12.1.2:6543`` but has something like `nginx `_ in front of it responding on ``https://api.mybusiness.com``. In this case the Ocelot ``BaseUrl`` should be ``https://api.mybusiness.com``. If you are using containers and require Ocelot to respond to clients on ``http://123.12.1.2:6543`` then you can do this, however if you are deploying multiple Ocelot's you will probably want to pass this on the command line in some kind of script. Hopefully whatever scheduler you are using can pass the IP. Program ------- Then in your `Program.cs `_ (with `top-level statements `_) you will want to have the following. .. code-block:: csharp :emphasize-lines: 9,11,21 using Ocelot.DependencyInjection; using Ocelot.Middleware; var builder = WebApplication.CreateBuilder(args); // Ocelot Basic setup builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); // single ocelot.json file in read-only mode builder.Services .AddOcelot(builder.Configuration); // Add your features if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } // Add middlewares aka app.Use*() var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); The main things to note are * ``builder.Configuration.AddOcelot()`` adds single `ocelot.json`_ configuration file in read-only mode. * ``builder.Services.AddOcelot(builder.Configuration)`` adds Ocelot required and default services [#f1]_ * ``app.UseOcelot()`` sets up all the Ocelot middlewares. Note, we have to await the threading result before calling ``app.RunAsync()`` * Do not add endpoint mappings (minimal API methods) such as ``app.MapGet()`` because the Ocelot pipeline is not compatible with them! .. _gettingstarted-samples: Samples ------- **Solution**: `Ocelot.Samples.sln`_ For beginners, we have prepared basic `samples `_ to help Ocelot newbies clone, compile, and get it running. * `Basic `_ sample: It has a single configuration file, `ocelot.json`_. * `Basic Configuration `_ sample: It has multiple configuration files (``ocelot.*.json``) to be merged into ``ocelot.json`` and written back to disk. After running in Visual Studio [#f2]_, you may use ``API.http`` files to send testing requests to the ``localhost`` Ocelot application instance. """" .. [#f1] The :ref:`di-services-addocelot-method` adds default ASP.NET services to the DI container. You can call another extended :ref:`di-addocelotusingbuilder-method` while configuring services to develop your own :ref:`di-custom-builder`. See more instructions in the ":ref:`di-addocelotusingbuilder-method`" section of the :doc:`../features/dependencyinjection` feature. .. [#f2] All :ref:`gettingstarted-samples` projects are organized as the `Ocelot.Samples.sln`_ file for Visual Studio 2022 IDE. .. _ocelot.json: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Basic/ocelot.json .. _Ocelot.Samples.sln: https://github.com/ThreeMammals/Ocelot/blob/main/samples/Ocelot.Samples.sln ================================================ FILE: docs/introduction/gotchas.rst ================================================ .. role:: htm(raw) :format: html .. role:: pdf(raw) :format: latex pdflatex Hosting Gotchas =============== Microsoft Learn: `Web server implementations in ASP.NET Core `_ Many errors and incidents (gotchas) are related to web server hosting scenarios. Please review deployment and web hosting common user scenarios below depending on your web server. .. _hosting-gotchas-iis: IIS --- | Repository Label: |image-IIS|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/labels/IIS}{IIS}` | Microsoft Learn: `Host ASP.NET Core on Windows with IIS `_ We **do not** recommend to deploy Ocelot app to IIS environments, but if you do, keep in mind the gotchas below. * When using ASP.NET Core 2.2+ and you want to use In-Process hosting, replace ``UseIISIntegration()`` with ``UseIIS()``, otherwise you will get startup errors. * Make sure you use Out-of-process hosting model instead of In-process one (see `Out-of-process hosting with IIS and ASP.NET Core `_), otherwise you will get very slow responses (see `1657`_). * Ensure all DNS servers of all downstream hosts are online and they function perfectly, otherwise you will get slow responses (see `1630`_). The community constanly reports `issues related to IIS `_. If you have some troubles in IIS environment to host Ocelot app, first of all, read open/closed issues, and after that, search for `IIS-related objects`_ in the repository. Probably you will find a ready solution by Ocelot community members. Finally, there is the special |image-IIS|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/labels/IIS}{IIS}` label for all `IIS-related objects`_. Feel free to put this label onto `issues `_, `pull requests `_, `discussions `_, etc. .. |image-IIS| image:: ../images/label-IIS-c5def5.svg :alt: label IIS :class: img-valign-bottom :target: https://github.com/ThreeMammals/Ocelot/labels/IIS .. _IIS-related objects: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20IIS&type=code .. _hosting-gotchas-kestrel: Kestrel ------- | Repository Label: |image-Kestrel|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/labels/Kestrel}{Kestrel}` | Microsoft Learn: `Kestrel web server in ASP.NET Core `_ We **do** recommend to deploy Ocelot app to self-hosting environments, aka Kestrel vs Docker. We try to optimize Ocelot web app for Kestrel & Docker hosting scenarios, but keep in mind the following gotchas. **1. Upload and download large files** [#f1]_ This is proxying the large content through the gateway: when you pump large (static) files using the gateway. We believe that your client apps should have direct integration to (static) files persistent storages and services: remote & destributed file systems, CDNs, static files & blob storages, etc. We **do not** recommend to pump large files (100Mb+ or even larger 1GB+) using gateway because of performance reasons: consuming memory and CPU, long delay times, producing network errors for downstream streaming, impact on other routes. | The community constanly reports issues related to `large files `_, ``application/octet-stream`` content type, :ref:`chunked-encoding`, etc., see issues `749`_, `1472`_. | If you still want to pump large files through an Ocelot gateway instance, use `23.0`_ version and higher. | In case of some errors, see the next point. **2. Maximum request body size** Docs: `Maximum request body size | Configure options for the ASP.NET Core Kestrel web server `_. ASP.NET ``HttpRequest`` behaves erroneously for application instances that do not have their Kestrel `MaxRequestBodySize `_ option configured correctly and having pumped large files of unpredictable size which exceeds the limit. As a quick fix, use this configuration recipe: .. code-block:: csharp var builder = WebApplication.CreateBuilder(args); builder.WebHost.ConfigureKestrel((context, serverOptions) => { int myVideoFileMaxSize = 1_073_741_824; // assume your file storage has max file size as 1 GB (1_073_741_824) int totalSize = myVideoFileMaxSize + 26_258_176; // and add some extra size serverOptions.Limits.MaxRequestBodySize = totalSize; // 1_100_000_000 thus 1 GB file should not exceed the limit }); .. _break: http://break.do Finally, there is the special |image-Kestrel|:pdf:`\href{https://github.com/ThreeMammals/Ocelot/labels/Kestrel}{Kestrel}` label for all `Kestrel-related objects `_. Feel free to put this label onto `issues `__, `pull requests `__, `discussions `__, etc. .. |image-Kestrel| image:: ../images/label-Kestrel-c5def5.svg :alt: label Kestrel :class: img-valign-bottom :target: https://github.com/ThreeMammals/Ocelot/labels/Kestrel """" .. [#f1] Large files pumping is stabilized and available as complete solution starting in `23.0`_ release. We believe our PRs `1724`_, `1769`_ helped to resolve the issues and stabilize large content proxying problems of `22.0.1`_ version and lower. .. _22.0.1: https://github.com/ThreeMammals/Ocelot/releases/tag/22.0.1 .. _23.0: https://github.com/ThreeMammals/Ocelot/releases/tag/23.0.0 .. _749: https://github.com/ThreeMammals/Ocelot/issues/749 .. _1472: https://github.com/ThreeMammals/Ocelot/issues/1472 .. _1657: https://github.com/ThreeMammals/Ocelot/issues/1657 .. _1630: https://github.com/ThreeMammals/Ocelot/issues/1630 .. _1724: https://github.com/ThreeMammals/Ocelot/pull/1724 .. _1769: https://github.com/ThreeMammals/Ocelot/pull/1769 ================================================ FILE: docs/introduction/notsupported.rst ================================================ Not Supported ============= Ocelot does not support... .. _chunked-encoding: Chunked Encoding ---------------- Ocelot will always get the body size and return `Content-Length `_ header. Sorry, if this doesn't work for your use case! Forwarding ``Host`` header -------------------------- The `Host `_ header that you send to Ocelot will not be forwarded to the downstream service. Obviously this would break everything. Swagger ------- Contributors have looked multiple times at building ``swagger.json`` out of the Ocelot ``ocelot.json`` but it doesnt fit into the vision the team has for Ocelot. If you would like to have Swagger in Ocelot then you must roll your own ``swagger.json`` and do the following in your `Program.cs `_. The code sample below registers a piece of middleware that loads your hand rolled ``swagger.json`` and returns it on ``/swagger/v1/swagger.json``. It then registers the SwaggerUI middleware from `Swashbuckle.AspNetCore `_ package: .. code-block:: csharp var builder = WebApplication.CreateBuilder(args); // ... var app = builder.Build(); app.Map("/swagger/v1/swagger.json", builder => builder.Run(async context => { var json = await File.ReadAllTextAsync("swagger.json"); await context.Response.WriteAsync(json); })); app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "Ocelot"); }); await app.UseOcelot(); await app.RunAsync(); The main reasons why we don't think Swagger makes sense is we already hand roll our definition in ``ocelot.json``. If we want people developing against Ocelot to be able to see what routes are available then either share the ``ocelot.json`` with them (This should be as easy as granting access to a repo etc) or use the Ocelot :doc:`../features/administration` API so that they can query Ocelot for the configuration. In addition to this, many people will configure Ocelot to proxy all traffic like ``/products/{everything}`` to their product service and you would not be describing what is actually available if you parsed this and turned it into a Swagger path. Also Ocelot has no concept of the models that the downstream services can return and linking to the above problem the same endpoint can return multiple models. Ocelot does not know what models might be used in POST, PUT etc, so it all gets a bit messy, and finally, the Swashbuckle package **does not** reload ``swagger.json`` if it changes during runtime. Ocelot's configuration can change during runtime so the Swagger and Ocelot information would not match. Unless we rolled our own Swagger implementation. If the developer wants something to easily test against the Ocelot API then we suggest using `Postman `_ as a simple way to do this. It might even be possible to write something that maps ``ocelot.json`` to the Postman JSON spec. However we do not intend to do this. ================================================ FILE: docs/make.bat ================================================ @ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo The 'sphinx-build' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the 'sphinx-build' executable. echo Alternatively you may add the Sphinx directory to PATH. echo If you don't have Sphinx installed, grab it from https://www.sphinx-doc.org/ exit /b 1 ) set command="%1" &:: html, clean and etc. call :dequote %command% echo Doing %ret% ... IF %command% == "" ( set status="FAILED" echo There is no build command! Available commands: clean, html echo See Sphinx Help below. %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% ) ELSE ( %SPHINXBUILD% -M %command% %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% set status="DONE" ) call :dequote %status% echo Build %ret% popd :dequote setlocal set thestring=%~1 endlocal&set ret=%thestring% goto :eof ================================================ FILE: docs/make.ps1 ================================================ # Command file for Sphinx documentation # PowerShell version without env var usage param ( [string]$command ) # Define sphinx-build as a normal variable $sphinxBuild = "sphinx-build" $SOURCEDIR = "." $BUILDDIR = "_build" Write-Host "Doing $command ..." if ([string]::IsNullOrEmpty($command)) { $status = "FAILED" Write-Host "There is no build command! Available commands: clean, html" Write-Host "See Sphinx Help below." & $sphinxBuild -M help $SOURCEDIR $BUILDDIR $SPHINXOPTS $O } else { & $sphinxBuild -M $command $SOURCEDIR $BUILDDIR $SPHINXOPTS $O $status = "DONE" } Write-Host "Build $status" ================================================ FILE: docs/make.sh ================================================ #!/bin/sh # # Command file for Sphinx documentation # if [ "$SPHINXBUILD" == "" ] then SPHINXBUILD="sphinx-build" fi SOURCEDIR="." BUILDDIR="_build" command=$1 # html, clean and etc. echo Doing $command ... if [ "$command" == "" ] then status="FAILED" echo There is no build command! Available commands: clean, html echo See Sphinx Help below. $SPHINXBUILD -M help $SOURCEDIR $BUILDDIR $SPHINXOPTS $O else $SPHINXBUILD -M $command $SOURCEDIR $BUILDDIR $SPHINXOPTS $O status="DONE" fi echo Build $status ================================================ FILE: docs/readme.md ================================================ # Ocelot Documentation The folder contains the documentation for Ocelot, build tools and configuration. We are using [Read the Docs](https://about.readthedocs.com) to host the documentation and the rendered version can be found [here](https://ocelot.readthedocs.io). Doc pages are authored in [reStructuredText](https://www.sphinx-doc.org/en/master/usage/restructuredtext/index.html) (reST). You can find a primer [here](https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html). You can find more information about [reST](https://www.sphinx-doc.org/en/master/usage/restructuredtext/) markup and [Sphinx](https://github.com/sphinx-doc/sphinx) under the following links: * [Read the Docs documentation](https://docs.readthedocs.io) * [GitHub: The Sphinx documentation generator](https://github.com/sphinx-doc/sphinx) * [Sphinx documentation](https://www.sphinx-doc.org/) * [YouTube: Sphinx & Read the Docs by Mahdi Yusuf](https://www.youtube.com/watch?v=oJsUvBQyHBs) ================================================ FILE: docs/releasenotes.rst ================================================ .. _24.1: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _24.1.0: https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 .. _25.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0 .. _25.0.0: https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0 .. _.NET 9: https://dotnet.microsoft.com/en-us/download/dotnet/9.0 .. _.NET 10: https://github.com/ThreeMammals/Ocelot/milestone/13 .. _Globality: https://github.com/ThreeMammals/Ocelot/milestone/9 .. _Ocelot: https://www.nuget.org/packages/Ocelot .. role:: htm(raw) :format: html .. _welcome: ####### Welcome ####### Welcome to the Ocelot `25.0`_ documentation! It is recommended to read all :ref:`release-notes` if you have deployed the Ocelot app in a production environment and are planning to upgrade to major, minor or patched versions. .. The major version `25.0.0`_ includes several patches, the history of which is outlined below. .. .. admonition:: Patches .. - `24.1.1`_, on July 16, 2025: Issue `2299`_ patch ... .. _release-notes: Release Notes ------------- .. _Ocelot.Provider.Kubernetes: https://www.nuget.org/packages/Ocelot.Provider.Kubernetes/ .. _Obsolete attributes: https://github.com/search?q=repo%3AThreeMammals%2FOcelot%20%5BObsolete&type=code | Release Tag: `25.0.0`_ | Release Codename: `.NET 10`_ .. In this minor release, the Ocelot team put the spotlight on the :doc:`../features/configuration` feature as part of their semi-annual 2025 effort, with a particular focus on the :ref:`config-global-configuration-schema`. .. This release enhances support for global configurations across both routing modes: the classic static :doc:`../features/routing` and the :doc:`service discovery <../features/servicediscovery>`-based :ref:`Dynamic Routing `. .. The updated documentation highlights `the deprecation `_ of certain options through multiple notes and warnings. .. This deprecation process will be completed in the upcoming `.NET 10`_ release. .. With the `Obsolete attributes`_ in place, C# developers will notice several warnings in the build logs during compilation. .. On top of that, this release brings a great enhancement to the :doc:`../features/kubernetes` provider, also known as the `Ocelot.Provider.Kubernetes`_ package. .. What's New? .. ----------- .. .. _@raman-m: https://github.com/raman-m .. .. _@kick2nick: https://github.com/kick2nick .. .. _@hogwartsdeveloper: https://github.com/hogwartsdeveloper .. .. _@RaynaldM: https://github.com/RaynaldM .. .. _585: https://github.com/ThreeMammals/Ocelot/issues/585 .. .. _2073: https://github.com/ThreeMammals/Ocelot/pull/2073 .. .. _2081: https://github.com/ThreeMammals/Ocelot/pull/2081 .. .. _2174: https://github.com/ThreeMammals/Ocelot/pull/2174 .. .. _Dynamic routing global configuration: https://github.com/ThreeMammals/Ocelot/issues/585 .. .. _KubeClient: https://www.nuget.org/packages/KubeClient/ .. .. _Polly: https://www.nuget.org/packages/Polly/ .. .. _Ocelot.Provider.Polly: https://www.nuget.org/packages/Ocelot.Provider.Polly .. .. _FailureRatio and SamplingDuration parameters of Polly V8 circuit-breaker: https://github.com/ThreeMammals/Ocelot/issues/2080 .. - :doc:`../features/configuration`: The "`Dynamic routing global configuration`_" feature has been redesigned by `@raman-m`_ and contributors. .. This update brings changes to the :ref:`config-dynamic-route-schema` and :ref:`config-global-configuration-schema`, while the :ref:`config-route-schema` stays the same apart from deprecation updates. .. All work was coordinated under issue `585`_, which addressed the challenges of configuring Ocelot's most popular features globally before version `24.1`_, when :ref:`dynamic routing ` gained global configuration partial support, but static routing mostly lacked it. .. A key outcome of `585`_ is the ability to override global configuration options within the ``DynamicRoutes`` collection. .. This ongoing issue will continue to require attention, as adapting static route global configurations for :ref:`dynamic routing ` is complex and, in some cases, impossible. .. This will be a challenge for future `Ocelot`_ releases and the community. .. - :doc:`../features/kubernetes`: The ":ref:`Kubernetes provider based on watch requests `" feature by `@kick2nick`_ in pull request `2174`_. .. The `Ocelot.Provider.Kubernetes`_ package now features a new :ref:`WatchKube provider ` for :doc:`Kubernetes <../features/kubernetes>` service discovery. .. This provider is a great fit for high-load environments where the older :ref:`Kube ` and :ref:`PollKube ` providers struggle to handle heavy traffic, often leading to increased log errors, HTTP 500 issues, and potential Ocelot instance failures. .. ``WatchKube`` is the next step in the evolution of these providers, leveraging the reactive capabilities of the `KubeClient`_ API. .. For guidance on choosing the right provider for your Kubernetes setup, check out the ":ref:`k8s-comparing-providers`" section. .. - :doc:`../features/configuration`: The ":ref:`Routing default timeout `" feature by `@hogwartsdeveloper`_ in pull request `2073`_. .. In the past, the ``Timeout`` setting in the :ref:`config-route-schema` did not actually stop requests, defaulting instead to a fixed `90 seconds `_. .. Custom timeouts were handled using the :doc:`../features/qualityofservice` :ref:`qos-timeout-strategy`, and this only applied if `Polly`_ and the `Ocelot.Provider.Polly`_ package were used. .. Now, the ``Timeout`` option (in seconds) can be set at the route, global, and QoS levels. .. The :ref:`config-global-configuration-schema` and :ref:`config-dynamic-route-schema` also include the new ``Timeout`` setting, making it possible to configure default timeouts for :ref:`dynamic routing ` as well. .. - :doc:`../features/qualityofservice`: The "`FailureRatio and SamplingDuration parameters of Polly V8 circuit-breaker`_" feature by `@RaynaldM`_ in pull request `2081`_. .. Starting with version `24.1`_, two new options in :ref:`qos-schema`, ``FailureRatio`` and ``SamplingDuration``, let you fine-tune the behavior of the :ref:`qos-circuit-breaker-strategy`. .. Both can be :ref:`configured globally `, even with :ref:`dynamic routing `. .. .. note:: The ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue`` options are now deprecated in `24.1`_, so check the ":ref:`qos-schema`" documentation for details. .. What's Updated? .. --------------- .. .. _@marklonquist: https://github.com/marklonquist .. .. _@jlukawska: https://github.com/jlukawska .. .. _@MiladRv: https://github.com/MiladRv .. .. _1592: https://github.com/ThreeMammals/Ocelot/pull/1592 .. .. _1659: https://github.com/ThreeMammals/Ocelot/pull/1659 .. .. _2114: https://github.com/ThreeMammals/Ocelot/pull/2114 .. .. _2294: https://github.com/ThreeMammals/Ocelot/pull/2294 .. .. _2295: https://github.com/ThreeMammals/Ocelot/pull/2295 .. .. _2324: https://github.com/ThreeMammals/Ocelot/pull/2324 .. .. _2331: https://github.com/ThreeMammals/Ocelot/pull/2331 .. .. _2332: https://github.com/ThreeMammals/Ocelot/pull/2332 .. .. _2336: https://github.com/ThreeMammals/Ocelot/pull/2336 .. .. _2339: https://github.com/ThreeMammals/Ocelot/pull/2339 .. .. _2342: https://github.com/ThreeMammals/Ocelot/pull/2342 .. .. _2345: https://github.com/ThreeMammals/Ocelot/pull/2345 .. .. _2347: https://github.com/ThreeMammals/Ocelot/pull/2347 .. .. _File-model: https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot/Configuration/File .. .. _deprecated options: https://github.com/search?q=repo%3AThreeMammals%2FOcelot+deprecated+language%3AreStructuredText&type=code&l=reStructuredText .. .. _Ocelot.Testing: https://github.com/ThreeMammals/Ocelot/tree/24.0.0/test/Ocelot.Testing .. .. _extension packages: https://www.nuget.org/profiles/ThreeMammals .. .. _23.3: https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.0 .. .. _DevOps: https://github.com/ThreeMammals/Ocelot/labels/DevOps .. .. _GH-Actions: https://github.com/ThreeMammals/Ocelot/actions .. - :doc:`../features/configuration`: Several `File-model`_ options have been deprecated by `@raman-m`_. .. The updated docs now highlight these `deprecated options`_ with multiple notes and warnings. .. The `24.1`_ deprecation process will wrap up in the upcoming `.NET 10`_ release. .. Due to the `Obsolete attributes`_, C# developers will notice several build warnings during compilation. .. - :ref:`b-testing`: The `Ocelot.Testing`_ project was deprecated by `@raman-m`_ in pull request `2295`_. .. The project was removed from the main repo and moved to its own `Ocelot.Testing `__ repository. .. This change allows the `Ocelot.Testing `__ package to be shared independently for delivery of `extension packages`_. .. The Ocelot team also plans to deprecate more projects and move them to separate repos because: .. **a)** despite the fact that a monorepo enables faster builds and quicker delivery; .. **b)** but the release process can be delayed by missing versions of integrated libraries in `extension packages`_. .. The goal is for the Ocelot repo to only contain essential projects, avoiding delays caused by integrated package release schedules. .. Legacy or abandoned integrated packages should be deprecated and maintained in their own repos with independent release cycles. .. - :doc:`../features/headerstransformation`: Added :ref:`global configuration ` by `@marklonquist`_ in pull request `1659`_. .. The :ref:`config-global-configuration-schema` now includes new ``DownstreamHeaderTransform`` and ``UpstreamHeaderTransform`` options. .. These work only with static routes, meaning the ``Routes`` collection (see :ref:`config-route-schema`). .. They are not supported for dynamic routes because they are not part of the :ref:`config-dynamic-route-schema`, and Ocelot Core does not read global configuration of this feature in :ref:`dynamic routing ` mode. .. This is noted in the :ref:`ht-roadmap` documentation. .. - :doc:`../features/authentication`: Added :ref:`global configuration ` by `@jlukawska`_ in pull request `2114`_. .. The :ref:`config-global-configuration-schema` now includes a new ``AuthenticationOptions`` property for setting up static routes globally. .. This also introduces the :ref:`AllowAnonymous boolean option ` within ``AuthenticationOptions`` to control static route authentication. .. Later, pull request `2336`_ extended global authentication support to dynamic routes. .. .. note:: The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_—see the ":ref:`authentication-options-schema`" documentation for details. .. - :doc:`../features/ratelimiting`: Re-designed :ref:`global configuration ` by `@MiladRv`_ and `@raman-m`_ in pull request `2294`_. .. The :ref:`config-global-configuration-schema` now includes a new ``RateLimitOptions`` property for both static and dynamic routes. .. Previously, global configuration was available through ``RateLimitOptions`` in :ref:`dynamic routing ` mode, while route overriding used the now-deprecated ``RateLimitRule`` from the :ref:`config-dynamic-route-schema`. .. This marks the second major overhaul of the *Rate Limiting* feature since the first update in pull request `1592`_. .. A new ``Wait`` option has been added, replacing the deprecated ``PeriodTimespan``, to enhance the :ref:`Fixed Window ` algorithm. .. The full list of deprecated options can be found in the ":ref:`Deprecated options `" documentation. .. - :doc:`../features/loadbalancer`: Added :ref:`global configuration ` by `@raman-m`_ in pull request `2324`_. .. The :ref:`config-global-configuration-schema` now includes a new ``LoadBalancerOptions`` property for both static and dynamic routes. .. Previously, global configuration was available through ``LoadBalancerOptions`` in :ref:`dynamic routing ` mode without dynamic route overrides. .. Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``LoadBalancerOptions`` for overriding, and global configuration for static routes is now supported as well. .. - :doc:`../features/caching`: Added :ref:`global configuration ` by `@raman-m`_ in pull request `2331`_. .. The :ref:`config-global-configuration-schema` now includes a new ``CacheOptions`` property for both static and dynamic routes. .. Global configuration has been available for static routes since version `23.3`_, but starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``CacheOptions`` for overriding. .. .. note:: .. The ``FileCacheOptions`` property in the :ref:`config-route-schema` (static routes) is deprecated in version `24.1`_. .. For more details, see the caching :ref:`caching-configuration` documentation. .. - :ref:`Http Handler `: Added :ref:`global configuration ` by `@raman-m`_ in pull request `2332`_. .. The :ref:`config-global-configuration-schema` now includes a new ``HttpHandlerOptions`` property for both static and dynamic routes. .. Previously, global configuration was available through ``HttpHandlerOptions`` in :ref:`dynamic routing ` mode without dynamic route overriding. .. Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``HttpHandlerOptions`` for overriding, and global configuration is now available for static routes as well. .. - :doc:`../features/authentication`: Added :ref:`global configuration ` by `@raman-m`_ in pull request `2336`_. .. The :ref:`config-global-configuration-schema` now includes a new ``AuthenticationOptions`` property for both static and dynamic routes. .. Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` also supports ``AuthenticationOptions`` to override global settings. .. .. note:: .. The ``AuthenticationProviderKey`` option is deprecated in version `24.1`_, so check the ":ref:`authentication-options-schema`" documentation for details. .. - :doc:`../features/qualityofservice`: Added :ref:`global configuration ` by `@raman-m`_ in pull request `2339`_. .. The :ref:`config-global-configuration-schema` now includes a new ``QoSOptions`` property for both static and dynamic routes. .. Previously, global configuration was available through ``QoSOptions`` in :ref:`dynamic routing ` mode without the option for dynamic route overrides. .. Starting with version `24.1`_, the :ref:`config-dynamic-route-schema` supports ``QoSOptions`` for overriding, and global configuration support is now available for static routes as well. .. .. note:: .. The ``DurationOfBreak``, ``ExceptionsAllowedBeforeBreaking``, and ``TimeoutValue`` options are deprecated in version `24.1`_. .. For details, see the ":ref:`qos-schema`" documentation. .. - `DevOps`_: Stabilized tests and reviewed `GH-Actions`_ workflows by `@raman-m`_ in pull requests `2342`_ and `2345`_. .. These efforts kept the CI/CD builds in `GitHub Actions `_ stable, targeting the `alpha release `_ of version `24.1`_. .. The CI/CD environment was set up and tested `GH-Actions`_ workflows in advance for the beta release, which is the goal of pull request `2347`_. .. Patches Included .. ---------------- .. .. _@mehyaa: https://github.com/mehyaa .. .. _913: https://github.com/ThreeMammals/Ocelot/issues/913 .. .. _930: https://github.com/ThreeMammals/Ocelot/issues/930 .. .. _1478: https://github.com/ThreeMammals/Ocelot/pull/1478 .. .. _2091: https://github.com/ThreeMammals/Ocelot/pull/2091 .. .. _2304: https://github.com/ThreeMammals/Ocelot/issues/2304 .. .. _2335: https://github.com/ThreeMammals/Ocelot/pull/2335 .. .. _RFC 8693: https://datatracker.ietf.org/doc/html/rfc8693 .. - :doc:`../features/websockets`: Issue `930`_ patch by `@hogwartsdeveloper`_ in pull request `2091`_. .. This update removes the troublesome ``System.Net.WebSockets.WebSocketException`` from logs, preventing Ocelot from running into 500 status disasters. .. The issue stemmed from client-side or network events that Ocelot's ``WebSocketsProxyMiddleware`` could not anticipate on the server side. .. The patch now checks for incorrect connection statuses, attempting to close the connection and end server-side tasks gracefully without errors. .. - :doc:`../features/kubernetes`: Issue `2304`_ patch by `@raman-m`_ in pull request `2335`_. .. This update fixes the :ref:`PollKube provider ` to address a bug with the first cold request, where the winning thread got an empty collection before the initial callback was triggered. .. The solution is to call the integrated discovery provider for the first cold request when the queue is empty. .. - :doc:`../features/authorization`: Issue `913`_ patch by `@mehyaa`_ in pull request `1478`_. .. Starting with version `24.1`_, Ocelot now supports `RFC 8693`_ (OAuth 2.0 Token Exchange) for the '``scope``' claim in the ``ScopesAuthorizer`` service, also referred to as the ``IScopesAuthorizer`` service in the DI container. .. This is noted in the ":ref:`authentication-allowed-scopes`" documentation (see the first note). Contributing ------------ .. |octocat| image:: images/octocat.png :alt: octocat :height: 25 :class: img-valign-middle :target: https://github.com/ThreeMammals/Ocelot/ .. _Pull requests: https://github.com/ThreeMammals/Ocelot/pulls .. _issues: https://github.com/ThreeMammals/Ocelot/issues .. _Ocelot GitHub: https://github.com/ThreeMammals/Ocelot/ .. _Ocelot Discussions: https://github.com/ThreeMammals/Ocelot/discussions .. _ideas: https://github.com/ThreeMammals/Ocelot/discussions/categories/ideas .. _questions: https://github.com/ThreeMammals/Ocelot/discussions/categories/q-a `Pull requests`_, `issues`_, and commentary are welcome at the `Ocelot GitHub`_ repository. For `ideas`_ and `questions`_, please post them in the `Ocelot Discussions`_ space. |octocat| Our :doc:`../building/devprocess` is a part of successful :doc:`../building/releaseprocess`. If you are a new contributor, it is crucial to read :doc:`../building/devprocess` attentively to grasp our methods for efficient and swift feature delivery. We, as a team, advocate adhering to :ref:`dev-best-practices` throughout the development phase. We extend our best wishes for your successful contributions to the Ocelot product! |octocat| ================================================ FILE: docs/requirements.txt ================================================ # Defining the exact version will make sure things don't break sphinx==9.0.4 alabaster==1.0.0 sphinx_copybutton==0.5.2 ================================================ FILE: postman/ocelot.postman_collection.json ================================================ { "id": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "name": "Ocelot", "description": "", "order": [ "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", "c4494401-3985-a5bf-71fb-6e4171384ac6", "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", "e8825dc3-4137-99a7-0000-ef5786610dc3", "fddfc4fa-5114-69e3-4744-203ed71a526b", "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", "4684c2fa-f38c-c193-5f55-bf563a1978c6", "5f308240-79e3-cf74-7a6b-fe462f0d54f1", "178f16da-c61b-c881-1c33-9d64a56851a4", "26a08569-85f6-7f9a-726f-61be419c7a34" ], "folders": [], "timestamp": 0, "owner": "212120", "public": false, "requests": [ { "folder": null, "id": "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", "name": "GET http://localhost:5000/comments?postId=1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/comments?postId=1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "id": "178f16da-c61b-c881-1c33-9d64a56851a4", "headers": "Authorization: Bearer {{AccessToken}}\n", "url": "http://localhost:5000/administration/configuration", "preRequestScript": null, "pathVariables": {}, "method": "GET", "data": null, "dataMode": "params", "tests": null, "currentHelper": "normal", "helperAttributes": {}, "time": 1508914722969, "name": "GET http://localhost:5000/admin/configuration", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "id": "26a08569-85f6-7f9a-726f-61be419c7a34", "headers": "", "url": "http://localhost:5000/administration/connect/token", "preRequestScript": null, "pathVariables": {}, "method": "POST", "data": [ { "key": "client_id", "value": "raft", "type": "text", "enabled": true }, { "key": "client_secret", "value": "REALLYHARDPASSWORD", "type": "text", "enabled": true }, { "key": "scope", "value": "admin raft ", "type": "text", "enabled": true }, { "key": "username", "value": "admin", "type": "text", "enabled": false }, { "key": "password", "value": "secret", "type": "text", "enabled": false }, { "key": "grant_type", "value": "client_credentials", "type": "text", "enabled": true } ], "dataMode": "params", "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"AccessToken\", jsonData.access_token);\npostman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);", "currentHelper": "normal", "helperAttributes": {}, "time": 1513240031907, "name": "POST http://localhost:5000/admin/connect/token copy copy", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "4684c2fa-f38c-c193-5f55-bf563a1978c6", "name": "DELETE http://localhost:5000/posts/1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "DELETE", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "id": "5f308240-79e3-cf74-7a6b-fe462f0d54f1", "headers": "Authorization: Bearer {{AccessToken}}\n", "url": "http://localhost:5000/administration/.well-known/openid-configuration", "preRequestScript": null, "pathVariables": {}, "method": "GET", "data": null, "dataMode": "params", "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "time": 1488038888813, "name": "GET http://localhost:5000/admin/.well-known/openid-configuration", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "folder": null, "rawModeData": null, "descriptionFormat": null, "queryParams": [], "headerData": [ { "key": "Authorization", "value": "Bearer {{AccessToken}}", "description": "", "enabled": true } ], "pathVariableData": [] }, { "id": "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", "folder": null, "name": "GET http://localhost:5000/posts", "dataMode": "params", "data": [ { "key": "client_id", "value": "admin", "type": "text", "enabled": true }, { "key": "client_secret", "value": "secret", "type": "text", "enabled": true }, { "key": "scope", "value": "admin", "type": "text", "enabled": true }, { "key": "username", "value": "admin", "type": "text", "enabled": true }, { "key": "password", "value": "admin", "type": "text", "enabled": true }, { "key": "grant_type", "value": "password", "type": "text", "enabled": true } ], "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "POST", "pathVariables": {}, "url": "http://localhost:5000/admin/configuration", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "c4494401-3985-a5bf-71fb-6e4171384ac6", "name": "GET http://localhost:5000/posts/1/comments", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/posts/1/comments", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", "name": "PATCH http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "descriptionFormat": "html", "description": "", "headers": "", "method": "PATCH", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "rawModeData": "{\n \"title\": \"gfdgsgsdgsdfgsdfgdfg\",\n}" }, { "folder": null, "id": "e8825dc3-4137-99a7-0000-ef5786610dc3", "name": "POST http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "descriptionFormat": "html", "description": "", "headers": "", "method": "POST", "pathVariables": {}, "url": "http://localhost:5000/posts", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" }, { "folder": null, "id": "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", "name": "GET http://localhost:5000/posts/1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "fddfc4fa-5114-69e3-4744-203ed71a526b", "name": "PUT http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "descriptionFormat": "html", "description": "", "headers": "", "method": "PUT", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": {}, "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}" } ] } ================================================ FILE: samples/Basic/API.http ================================================ @OcelotHttp = http://localhost:5555 GET {{OcelotHttp}}/ocelot/posts/1 Accept: application/json ### GET {{OcelotHttp}}/ocelot/docs/ Accept: text/html, */* ### @OcelotHttps = https://localhost:7777 GET {{OcelotHttps}}/ocelot/posts/3 Accept: application/json ### GET {{OcelotHttps}}/ocelot/docs/releasenotes.html Accept: text/html, */* ### ================================================ FILE: samples/Basic/Ocelot.Samples.Basic.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Basic https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Basic/Program.cs ================================================ using Ocelot.DependencyInjection; using Ocelot.Middleware; var builder = WebApplication.CreateBuilder(args); // Ocelot Basic setup builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); // single ocelot.json file in read-only mode builder.Services .AddOcelot(builder.Configuration); // Add your features if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } // Add middlewares aka app.Use*() var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); ================================================ FILE: samples/Basic/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "ocelot/posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5555" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "ocelot/docs/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7777;http://localhost:5555" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "ocelot/posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7777/ocelot/docs/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7777;http://localhost:5555" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62555/", "sslPort": 44355 } } } ================================================ FILE: samples/Basic/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Basic/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Basic/ocelot.json ================================================ { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/posts/{id}", "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ] }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/docs/{everything}", "DownstreamPathTemplate": "/en/latest/{everything}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ] }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/_/{BFF}", "DownstreamPathTemplate": "/_/{BFF}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ] } ], "GlobalConfiguration": { "BaseUrl": "http://localhost:5555" // "https://localhost:7777" } } ================================================ FILE: samples/Basic/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Configuration/API.http ================================================ @HttpHost = http://localhost:5556 GET {{HttpHost}}/ocelot/posts/1 Accept: application/json ### GET {{HttpHost}}/ocelot/docs/ Accept: text/html, */* ### GET {{HttpHost}}/weather/current/London Accept: application/json ### @HttpsHost = https://localhost:7778 GET {{HttpsHost}}/ocelot/posts/3 Accept: application/json ### GET {{HttpsHost}}/ocelot/docs/releasenotes.html Accept: text/html, */* ### GET {{HttpsHost}}/weather/current/Paris Accept: application/json ### ================================================ FILE: samples/Configuration/Ocelot.Samples.Configuration.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Configuration https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Configuration/Program.cs ================================================ using Ocelot.DependencyInjection; using Ocelot.Middleware; var builder = WebApplication.CreateBuilder(args); // Ocelot Basic setup builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot("ocelot-configuration", builder.Environment); // multiple environment files (ocelot.*.json) to be merged to ocelot.json file and write it back to disk //.AddOcelot("ocelot-configuration", builder.Environment, MergeOcelotJson.ToMemory); // to be merged to ocelot.json JSON-data and keep it in memory builder.Services .AddOcelot(builder.Configuration); // Add your features if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } // Add middlewares aka app.Use*() var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); ================================================ FILE: samples/Configuration/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "http://localhost:5556", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "https://localhost:7778;http://localhost:5556", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/Configuration/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Configuration/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Configuration/ocelot-configuration/ocelot.docs.json ================================================ { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/docs/{everything}", "DownstreamPathTemplate": "/en/latest/{everything}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ] }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/_/{BFF}", "DownstreamPathTemplate": "/_/{BFF}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ] } ] } ================================================ FILE: samples/Configuration/ocelot-configuration/ocelot.global.json ================================================ { "GlobalConfiguration": { "BaseUrl": "http://localhost:5556" // "https://localhost:7778" } } ================================================ FILE: samples/Configuration/ocelot-configuration/ocelot.posts.json ================================================ { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/posts/{id}", "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ] } ] } ================================================ FILE: samples/Configuration/ocelot-configuration/ocelot.weather.json ================================================ { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/weather/current/{city}", "DownstreamPathTemplate": "/v1/current.json?q={city}&key=4ea9a1d2aafe4e15bbd173615242312", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "api.weatherapi.com", "Port": 443 } ] } ] } ================================================ FILE: samples/Configuration/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Eureka/ApiGateway/Ocelot.Samples.Eureka.ApiGateway.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Eureka README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Eureka/ApiGateway/Program.cs ================================================ using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Provider.Eureka; using Ocelot.Provider.Polly; using Ocelot.Samples.Web; //_ = OcelotHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); // Ocelot Basic setup builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services .AddOcelot(builder.Configuration) .AddEureka() .AddPolly(); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); await app.UseOcelot(); app.Run(); ================================================ FILE: samples/Eureka/ApiGateway/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "Category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5557" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7779;http://localhost:5557" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7779/category/", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7779;http://localhost:5557" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62557/", "sslPort": 44357 } } } ================================================ FILE: samples/Eureka/ApiGateway/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Trace", "System": "Information", "Microsoft": "Information" } }, "spring": { "application": { "name": "Ocelot-Gateway" }, "cloud": { "config": { "uri": "http://localhost:5000", "validateCertificates": false } } }, "eureka": { "client": { "serviceUrl": "http://localhost:8761/eureka/", "shouldRegisterWithEureka": false, "validateCertificates": false } } } ================================================ FILE: samples/Eureka/ApiGateway/ocelot.json ================================================ { "Routes": [ { "ServiceName": "ncore-rat", "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/category", "DownstreamPathTemplate": "/api/category", "DownstreamScheme": "http", "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10000, "TimeoutValue": 5000 }, "FileCacheOptions": { "TtlSeconds": 15 } }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/nodiscovery/category", "DownstreamPathTemplate": "/api/category", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5558 } ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10000, "TimeoutValue": 5000 }, "FileCacheOptions": { "TtlSeconds": 15 } } ], "GlobalConfiguration": { "RequestIdKey": "OcRequestId", "AdministrationPath": "/administration", "ServiceDiscoveryProvider": { "Type": "Eureka" } } } ================================================ FILE: samples/Eureka/ApiGateway/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Eureka/DownstreamService/Controllers/CategoryController.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Ocelot.Samples.Eureka.DownstreamService.Controllers; [Route("api/[controller]")] public class CategoryController : Controller { // GET api/category [HttpGet] public IEnumerable Get() { return new[] { "category1", "category2" }; } } ================================================ FILE: samples/Eureka/DownstreamService/Ocelot.Samples.Eureka.DownstreamService.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Eureka README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Eureka/DownstreamService/Program.cs ================================================ using Steeltoe.Discovery.Client; var builder = WebApplication.CreateBuilder(args); builder.Services .AddDiscoveryClient(builder.Configuration) .AddControllers(); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection() .UseAuthorization(); app.MapControllers(); app.Run(); ================================================ FILE: samples/Eureka/DownstreamService/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/Category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5558" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7780;http://localhost:5558" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "api/category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7780/api/category", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7780;http://localhost:5558" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62558/", "sslPort": 44358 } } } ================================================ FILE: samples/Eureka/DownstreamService/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Eureka/DownstreamService/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "spring": { "application": { "name": "ncore-rat" }, "cloud": { "config": { "uri": "http://localhost:5001", "validate_certificates": false } } }, "eureka": { "client": { "serviceUrl": "http://localhost:8761/eureka/", "shouldFetchRegistry": false, "validateCertificates": false }, "instance": { "port": 5001 } } } ================================================ FILE: samples/Eureka/DownstreamService/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.Text.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==" }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.Text.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.Text.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==" }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Eureka/OcelotEureka.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.27428.2027 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DownstreamService", "./DownstreamService/DownstreamService.csproj", "{2982C147-9446-47FE-862E-E689B64CC7E7}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiGateway", "./ApiGateway/ApiGateway.csproj", "{006CF27E-5400-43E9-B511-C54EC1B9C546}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {2982C147-9446-47FE-862E-E689B64CC7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2982C147-9446-47FE-862E-E689B64CC7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {2982C147-9446-47FE-862E-E689B64CC7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU {2982C147-9446-47FE-862E-E689B64CC7E7}.Release|Any CPU.Build.0 = Release|Any CPU {006CF27E-5400-43E9-B511-C54EC1B9C546}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {006CF27E-5400-43E9-B511-C54EC1B9C546}.Debug|Any CPU.Build.0 = Debug|Any CPU {006CF27E-5400-43E9-B511-C54EC1B9C546}.Release|Any CPU.ActiveCfg = Release|Any CPU {006CF27E-5400-43E9-B511-C54EC1B9C546}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2C604707-2EA1-4CCF-A89C-22B613052C8D} EndGlobalSection EndGlobal ================================================ FILE: samples/Eureka/README.md ================================================ #Example how to use Eureka service discovery I created this becasue users are having trouble getting Eureka to work with Ocelot, hopefully this helps. Please review the implementation of the individual servics to understand how everything fits together. ##Instructions 1. Get Eureka installed and running... ``` $ git clone https://github.com/spring-cloud-samples/eureka.git $ cd eureka $ mvnw spring-boot:run ``` Leave the service running 2. Get Downstream service running and registered with Eureka ``` cd ./DownstreamService/ dotnet run ``` Leave the service running 3. Get API Gateway running and collecting services from Eureka ``` cd ./ApiGateway/ dotnet run ``` Leave the service running 4. Make a http request to http://localhost:5000/category you should get the following response ```json ["category1","category2"] ``` ================================================ FILE: samples/GraphQL/GraphQlDelegatingHandler.cs ================================================ using GraphQL; using GraphQL.NewtonsoftJson; using System.Net; using System.Net.Http.Headers; namespace Ocelot.Samples.GraphQL; public class GraphQLDelegatingHandler : DelegatingHandler { //private readonly ISchema _schema; private readonly IDocumentExecuter _executer; private readonly IGraphQLTextSerializer _serializer; public GraphQLDelegatingHandler(IDocumentExecuter executer, IGraphQLTextSerializer serializer) { _executer = executer; _serializer = serializer; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { //try get query from body, could check http method :) var query = await request.Content!.ReadAsStringAsync(cancellationToken); //if not body try query string, dont hack like this in real world.. if (query.Length == 0) { var decoded = WebUtility.UrlDecode(request.RequestUri!.Query); query = decoded.Replace("?query=", string.Empty); } var result = await _executer.ExecuteAsync(_ => { _.Query = query; }); // IGraphQLSerializer & IGraphQLTextSerializer: https://github.com/graphql-dotnet/graphql-dotnet/blob/master/docs2/site/docs/getting-started/transport.md#igraphqlserializer--igraphqltextserializer var responseBody = _serializer.Serialize(result); var media = new MediaTypeHeaderValue("application/graphql-response+json"); var response = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(responseBody, media), }; //ocelot will treat this like any other http request... return response; } } ================================================ FILE: samples/GraphQL/Models/Hero.cs ================================================ namespace Ocelot.Samples.GraphQL.Models; public class Hero { public int Id { get; set; } public required string Name { get; set; } } ================================================ FILE: samples/GraphQL/Models/Query.cs ================================================ using GraphQL; namespace Ocelot.Samples.GraphQL.Models; public class Query { private readonly List _heroes = new() { new Hero { Id = 1, Name = "R2-D2" }, new Hero { Id = 2, Name = "Batman" }, new Hero { Id = 3, Name = "Wonder Woman" }, new Hero { Id = 4, Name = "Tom Pallister" } }; [GraphQLMetadata("hero")] public Hero? GetHero(int id) { return _heroes.FirstOrDefault(x => x.Id == id); } } ================================================ FILE: samples/GraphQL/Ocelot.Samples.GraphQL.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/GraphQL README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/GraphQL/OcelotGraphQL.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.452 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotGraphQL", "OcelotGraphQL.csproj", "{5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A3220BF-FE0B-4B26-8B2F-37DB7292EEA9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5762C36C-EA4B-44D9-9DAA-2F7C3FC98692} EndGlobalSection EndGlobal ================================================ FILE: samples/GraphQL/Program.cs ================================================ using GraphQL.Types; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Samples.GraphQL; using Ocelot.Samples.GraphQL.Models; using Ocelot.Samples.Web; var schema = Schema.For(@" type Hero { id: Int name: String } type Query { hero(id: Int): Hero } ", _ => { _.Types.Include(); }); //_ = OcelotHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); // Ocelot Basic setup builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services .AddSingleton(schema) .AddOcelot(builder.Configuration) .AddDelegatingHandler(); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); await app.UseOcelot(); app.Run(); ================================================ FILE: samples/GraphQL/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5559" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7781;http://localhost:5559" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7781/graphql", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7781;http://localhost:5559" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62559/", "sslPort": 44359 } } } ================================================ FILE: samples/GraphQL/README.md ================================================ # Ocelot using GraphQL example Loads of people keep asking me if Ocelot will every support GraphQL, in my mind Ocelot and GraphQL are two different things that can work together. I would not try and implement GraphQL in Ocelot instead I would either have Ocelot in front of GraphQL to handle things like authorization / authentication or I would bring in the awesome [graphql-dotnet](https://github.com/graphql-dotnet/graphql-dotnet) library and use it in a [DelegatingHandler](http://ocelot.readthedocs.io/en/latest/features/delegatinghandlers.html). This way you could have Ocelot and GraphQL without the extra hop to GraphQL. This same is an example of how to do that. ## Example If you run this project with $ dotnet run Use postman or something to make the following requests and you can see Ocelot and GraphQL in action together... GET http://localhost:5000/graphql?query={ hero(id: 4) { id name } } RESPONSE ```json { "data": { "hero": { "id": 4, "name": "Tom Pallister" } } } ``` POST http://localhost:5000/graphql BODY ```json { hero(id: 4) { id name } } ``` RESPONSE ```json { "data": { "hero": { "id": 4, "name": "Tom Pallister" } } } ``` ## Notes Please note this project never goes out to another service, it just gets the data for GraphQL in memory. You would need to add the details of your GraphQL server in ocelot.json e.g. ```json { "Routes": [ { "DownstreamPathTemplate": "/graphql", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "yourgraphqlhost.com", "Port": 80 } ], "UpstreamPathTemplate": "/graphql", "DelegatingHandlers": [ "GraphQlDelegatingHandler" ] } ] } ``` ================================================ FILE: samples/GraphQL/ocelot.json ================================================ { "Routes": [ { "UpstreamPathTemplate": "/graphql", "DownstreamPathTemplate": "/", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "DelegatingHandlers": [ "GraphQLDelegatingHandler" ] } ] } ================================================ FILE: samples/GraphQL/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "GraphQL": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==", "dependencies": { "GraphQL-Parser": "9.5.0", "GraphQL.Analyzers": "8.8.4" } }, "GraphQL.NewtonsoftJson": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==", "dependencies": { "GraphQL": "[8.8.4, 9.0.0)", "Newtonsoft.Json": "13.0.3" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "GraphQL-Parser": { "type": "Transitive", "resolved": "9.5.0", "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" }, "GraphQL.Analyzers": { "type": "Transitive", "resolved": "8.8.4", "contentHash": "uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "GraphQL": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==", "dependencies": { "GraphQL-Parser": "9.5.0", "GraphQL.Analyzers": "8.8.4" } }, "GraphQL.NewtonsoftJson": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==", "dependencies": { "GraphQL": "[8.8.4, 9.0.0)", "Newtonsoft.Json": "13.0.3" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "GraphQL-Parser": { "type": "Transitive", "resolved": "9.5.0", "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" }, "GraphQL.Analyzers": { "type": "Transitive", "resolved": "8.8.4", "contentHash": "uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "GraphQL": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Ey/3R5ydkfriUG6PXiZPdeGkyLADQ/wZMgDBtHto5ZvSv+PtzpOD8YX0WAFvW3SA7Ldvwzw5S3RVBSB97imANA==", "dependencies": { "GraphQL-Parser": "9.5.0", "GraphQL.Analyzers": "8.8.4" } }, "GraphQL.NewtonsoftJson": { "type": "Direct", "requested": "[8.8.4, )", "resolved": "8.8.4", "contentHash": "Y3ZN5RpVEM43g4U36fVMkBBETWxjCaffVlJ1CqLPBWqmL7PF/IXKt1lQCvWXDY6AysAbiPaLk5hy/zbP3hvEWw==", "dependencies": { "GraphQL": "[8.8.4, 9.0.0)", "Newtonsoft.Json": "13.0.3" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "GraphQL-Parser": { "type": "Transitive", "resolved": "9.5.0", "contentHash": "5XWJGKHdVi8pyD4P0EglmJmlXEGs0HzvGlEBf3+/Ve1jLYBBKIOkKvY0Ej17b9Kn1bbBxkrmghqbmsMbkLL1nQ==" }, "GraphQL.Analyzers": { "type": "Transitive", "resolved": "8.8.4", "contentHash": "uzsOFK6wKUjefBhejWX6bzomjAM2tgqFWdjvTK26skYzGZPqA9r4Ra+AeOR3aARWlQetmB1T7nG5fIMfZfyLQw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Kubernetes/.dockerignore ================================================ .dockerignore .env .git .gitignore .vs .vscode */bin */obj **/.toolstarget ================================================ FILE: samples/Kubernetes/ApiGateway/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj", "ApiGateway/"] RUN dotnet restore "ApiGateway/Ocelot.Samples.OcelotKube.ApiGateway.csproj" COPY . . WORKDIR "/src/ApiGateway" RUN dotnet build "Ocelot.Samples.OcelotKube.ApiGateway.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Ocelot.Samples.OcelotKube.ApiGateway.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Ocelot.Samples.OcelotKube.ApiGateway.dll"] ================================================ FILE: samples/Kubernetes/ApiGateway/Ocelot.Samples.Kubernetes.ApiGateway.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Linux Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Kubernetes https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Kubernetes/ApiGateway/Program.cs ================================================ using KubeClient; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Provider.Kubernetes; using Ocelot.Samples.Web; //_ = OcelotHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); goto Case5; // Your case should be selected here!!! // Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-bool-method Case1: // Use a pod service account builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(); goto Start; // Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-bool-method Case2: // Don't use a pod service account, manually bind options Action configureOptions = opts => { opts.ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, "my-host", 443).Uri; opts.AccessToken = "my-token"; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AllowInsecure = true; }; builder.Services .AddOptions() .Configure(configureOptions); // mannual binding options via IOptions builder.Services .AddOcelot().AddKubernetes(false); // don't use pod service account goto Start; // Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-action-kubeclientoptions-method Case3: // Don't use a pod service account, manually bind options, ignore global ServiceDiscoveryProvider json-options Action myOptions = opts => { opts.ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, "my-host", 443).Uri; opts.AccessToken = "my-token"; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AllowInsecure = true; // here is wrong value! }; builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(myOptions); // configure options with action, without optional args goto Start; Case4: // Don't use a pod service account, manually bind options, ignore global ServiceDiscoveryProvider json-options builder.Services .AddKubeClientOptions(opts => { opts.ApiEndPoint = new UriBuilder("https", "my-host", 443).Uri; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AccessToken = "my-token"; opts.AllowInsecure = true; }) .AddOcelot(builder.Configuration) .AddKubernetes(false); // don't use pod service account, and client options provided via AddKubeClientOptions goto Start; // Link: https://github.com/ThreeMammals/Ocelot/blob/develop/docs/features/kubernetes.rst#addkubernetes-action-kubeclientoptions-method Case5: // Use global ServiceDiscoveryProvider json-options Action? none = null; builder.Services .AddOcelot(builder.Configuration) .AddKubernetes(null, allowInsecure: true /*optional args*/) // shorten version // or .AddKubernetes(none, allowInsecure: true /*optional args*/) // shorten version 2 // or .AddKubernetes(configureOptions: null, allowInsecure: true /*optional args*/); // don't configure options with action, but do with optional args Start: if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } await app.UseOcelot(); await app.RunAsync(); ================================================ FILE: samples/Kubernetes/ApiGateway/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5560" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7782;http://localhost:5560" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7782/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7782;http://localhost:5560" }, "distributionName": "" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://localhost:{ServicePort}/values" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62560/", "sslPort": 44360 } } } ================================================ FILE: samples/Kubernetes/ApiGateway/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Kubernetes/ApiGateway/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Kubernetes/ApiGateway/ocelot.json ================================================ { "Routes": [ { "ServiceName": "my-service", "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/values", "DownstreamPathTemplate": "/api/values", "DownstreamScheme": "http" }, { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/nodiscovery/values", "DownstreamPathTemplate": "/api/values", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5561 } ] } ], "GlobalConfiguration": { "ServiceDiscoveryProvider": { "Scheme": "https", "Host": "192.168.0.13", "Port": 443, "Token": "txpc696iUhbVoudg164r93CxDTrKRVWG", "Namespace": "dev", "Type": "Kube" } } } ================================================ FILE: samples/Kubernetes/ApiGateway/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==" }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==" }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==" }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Kubernetes/Dockerfile ================================================ FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 FROM microsoft/dotnet:2.1-sdk AS build WORKDIR /src COPY ["ApiGateway/ApiGateway.csproj", "ApiGateway/"] COPY ["../../src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj", "../../src/Ocelot.Provider.Polly/"] COPY ["../../src/Ocelot/Ocelot.csproj", "../../src/Ocelot/"] COPY ["../../src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj", "../../src/Ocelot.Provider.Kubernetes/"] RUN dotnet restore "ApiGateway/ApiGateway.csproj" COPY . . WORKDIR "/src/ApiGateway" RUN dotnet build "ApiGateway.csproj" -c Release -o /app FROM build AS publish RUN dotnet publish "ApiGateway.csproj" -c Release -o /app FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "ApiGateway.dll"] ================================================ FILE: samples/Kubernetes/DownstreamService/Controllers/ValuesController.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Ocelot.Samples.Kubernetes.DownstreamService.Controllers; [ApiController] [Route("api/[controller]")] public class ValuesController : ControllerBase { // GET api/values [HttpGet] public ActionResult> Get() { return new[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public ActionResult Get(int id) { return "value"; } // POST api/values [HttpPost] public void Post([FromBody] string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { } } ================================================ FILE: samples/Kubernetes/DownstreamService/Controllers/WeatherForecastController.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Ocelot.Samples.Kubernetes.DownstreamService.Controllers; using Models; [ApiController] [Route("api/[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger _logger; public WeatherForecastController(ILogger logger) { _logger = logger; } [HttpGet(Name = "GetWeatherForecast")] public IEnumerable Get() { return Enumerable.Range(1, 5) .Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } } ================================================ FILE: samples/Kubernetes/DownstreamService/Dockerfile ================================================ FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj", "DownstreamService/"] RUN dotnet restore "DownstreamService/Ocelot.Samples.OcelotKube.DownstreamService.csproj" COPY . . WORKDIR "/src/DownstreamService" RUN dotnet build "Ocelot.Samples.OcelotKube.DownstreamService.csproj" -c Release -o /app/build FROM build AS publish RUN dotnet publish "Ocelot.Samples.OcelotKube.DownstreamService.csproj" -c Release -o /app/publish /p:UseAppHost=false FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "Ocelot.Samples.OcelotKube.DownstreamService.dll"] ================================================ FILE: samples/Kubernetes/DownstreamService/Models/WeatherForecast.cs ================================================ namespace Ocelot.Samples.Kubernetes.DownstreamService.Models; public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string? Summary { get; set; } } ================================================ FILE: samples/Kubernetes/DownstreamService/Ocelot.Samples.Kubernetes.DownstreamService.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Linux Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Kubernetes https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Kubernetes/DownstreamService/Program.cs ================================================ using Ocelot.Samples.Web; using System.Text.Json; using System.Text.Json.Serialization; _ = DownstreamHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); builder.Services // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle .AddEndpointsApiExplorer() .AddSwaggerGen() .AddHttpClient() .AddControllers() .AddJsonOptions(options => { options.AllowInputFormatterExceptionMessages = true; var jOptions = options.JsonSerializerOptions; jOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); jOptions.PropertyNameCaseInsensitive = true; jOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; }); var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); ================================================ FILE: samples/Kubernetes/DownstreamService/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5561" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7783;http://localhost:5561" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7783/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7783;http://localhost:5561" }, "distributionName": "" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://localhost:{ServicePort}/swagger" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62561/", "sslPort": 44361 } } } ================================================ FILE: samples/Kubernetes/DownstreamService/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Kubernetes/DownstreamService/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Kubernetes/DownstreamService/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "10.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "8.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "jDM3a95WerM8g6IcMiBXq1qRS9dqmEUpgnCk2DeMWpPkYtp1ia+CkXabOnK93JmhVlUmv8l9WMPsCSUm+WqkIA==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "9.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "9.0.0", "contentHash": "1Kzzf7pRey40VaUkHN9/uWxrKVkLu2AQjt+GVeeKLLpiEHAJ1xZRsLSh4ZZYEnyS7Kt2OBOPmsXNdU+wbcOl5w==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/Kubernetes/OcelotKube.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.28803.202 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiGateway", "ApiGateway\ApiGateway.csproj", "{E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DownstreamService", "DownstreamService\DownstreamService.csproj", "{86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Debug|Any CPU.Build.0 = Debug|Any CPU {E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.ActiveCfg = Release|Any CPU {E9AFBFD7-EF20-48E5-BB30-5C63C59D7C1C}.Release|Any CPU.Build.0 = Release|Any CPU {86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Debug|Any CPU.Build.0 = Debug|Any CPU {86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.ActiveCfg = Release|Any CPU {86FFAE3C-648F-4CDE-A260-37C8EBFBF4F2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D29790E8-4BA9-4E60-8D7D-327E21320CC9} EndGlobalSection EndGlobal ================================================ FILE: samples/Metadata/API.http ================================================ @HostHttp = http://localhost:5139 GET {{HostAddress}}/ocelot/docs/ Accept: application/json ### Route #1 aka 'ocelot-docs' GET {{HostAddress}}/weather/current/London Accept: application/json ### route #3 aka 'weather-current' GET {{HostAddress}}/ocelot/posts/3 Accept: application/json ### Route #4 aka 'ocelot-posts' GET {{HostAddress}}/test/deflate Accept: application/json ### Route #5 GET {{HostAddress}}/test/gzip Accept: application/json ### Route #6 ############################################## @HostHttps = https://localhost:7252 GET {{HostHttps}}/ocelot/docs/ Accept: application/json ### Route #1 aka 'ocelot-docs' GET {{HostHttps}}/weather/current/London Accept: application/json ### route #3 aka 'weather-current' GET {{HostHttps}}/ocelot/posts/3 Accept: application/json ### Route #4 aka 'ocelot-posts' GET {{HostHttps}}/test/deflate Accept: application/json ### Route #5 GET {{HostHttps}}/test/gzip Accept: application/json ### Route #6 ================================================ FILE: samples/Metadata/MetadataResponder.cs ================================================ using Ocelot.Configuration; using Ocelot.Headers; using Ocelot.Metadata; using Ocelot.Middleware; using Ocelot.Responder; using System.IO.Compression; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using ZstdNet; namespace Ocelot.Samples.Metadata; public class MetadataResponder : HttpContextResponder { public MetadataResponder(IRemoveOutputHeaders removeOutputHeaders) : base(removeOutputHeaders) { } protected override async Task WriteToUpstreamAsync(HttpContext context, DownstreamResponse downstream) { // Ensure the route has metadata at all var route = context.Items.DownstreamRoute(); var metadata = route?.MetadataOptions.Metadata; if ((metadata?.Count ?? 0) == 0) { await base.WriteToUpstreamAsync(context, downstream); return; } // Content type is 'application/json', so embed route metadata JSON-node var response = context.Items.DownstreamResponse(); if (response.Content.Headers.ContentType?.MediaType == "application/json") { // Don't process json requested by scripts aka XHR if (route.GetMetadata("disableMetadataJson", false)) { AddMetadataHeader(context, metadata!); // but return in the header await base.WriteToUpstreamAsync(context, downstream); return; } //var json = await response.Content.ReadAsStringAsync(context.RequestAborted); var json = await ReadCompressedJsonAsync(context.Response, response.Content, context.RequestAborted); if (string.IsNullOrEmpty(json)) { // Impossible to decompress content and write to body // Write metadata to the contentEncoding and write original content AddMetadataHeader(context, metadata!); await base.WriteToUpstreamAsync(context, downstream); return; } //var json1 = await JsonNode.ParseAsync(jsonStream, cancellationToken: context.RequestAborted); var json1 = JsonObject.Parse(json); var json2 = JsonSerializer.SerializeToNode(metadata); var aggregated = new JsonObject { [nameof(HttpContext.Response)] = json1, [nameof(MetadataOptions.Metadata)] = json2, }; AddMetadataHeader(context, metadata!); await WriteJsonAsync(context.Response, response.Content, aggregated, context.RequestAborted); } else { AddMetadataHeader(context, metadata!); await base.WriteToUpstreamAsync(context, downstream); } } private static void AddMetadataHeader(HttpContext context, IDictionary metadata) { var node = JsonSerializer.SerializeToNode(metadata); var header = node?.ToJsonString(JsonSerializerOptions.Default/*Web*/) ?? string.Empty; context.Response.Headers.Append("OC-Route-Metadata", new(header)); } private static Encoding DetectEncoding(HttpContent content) { if (!string.IsNullOrEmpty(content.Headers.ContentType?.CharSet)) { try { return Encoding.GetEncoding(content.Headers.ContentType.CharSet); } catch (ArgumentException) { return Encoding.UTF8; // unknown encoding, fallback to UTF-8 } } return Encoding.UTF8; // default to UTF-8 } private static async Task ReadCompressedJsonAsync(HttpResponse response, HttpContent content, CancellationToken token) { var encoding = DetectEncoding(content); if (content.Headers.ContentEncoding.Contains("br")) // Brotli compression: https://www.prowaretech.com/articles/current/dot-net/compression-brotli { using var compressed = await content.ReadAsStreamAsync(token); var decompressed = await DecompressBrotliAsync(compressed, token); return encoding.GetString(decompressed); } else if (content.Headers.ContentEncoding.Contains("zstd")) // Zstandard compression: https://github.com/facebook/zstd { using var compressed = await content.ReadAsStreamAsync(token); var decompressed = await DecompressZstandardAsync(compressed, token); return encoding.GetString(decompressed); } else if (content.Headers.ContentEncoding.Contains("deflate")) // Deflate algorithm compression { // Actually it doesn't work: only MS compressed DeflateStream are supported // Decompressor will generate System.IO.InvalidDataException: The archive entry was compressed using an unsupported compression method. //var compressed = await content.ReadAsStreamAsync(token); //var decompressed = await DecompressDeflateAsync(compressed, token); //return encoding.GetString(decompressed); return string.Empty; } else if (content.Headers.ContentEncoding.Contains("gzip")) // GZip compression { var compressed = await content.ReadAsStreamAsync(token); var decompressed = DecompressGZip(compressed); return encoding.GetString(decompressed); } else // no compression { var buffer = await content.ReadAsByteArrayAsync(token); return encoding.GetString(buffer); } } private static Task WriteJsonAsync(HttpResponse to, HttpContent content, JsonObject json, CancellationToken token) { // We will not use original downstrean encoding, so defaults always to UTF8 for upstream var encoding = Encoding.UTF8; // DetectEncoding(content); var serialized = json.ToJsonString(JsonSerializerOptions.Default/*Web*/); // will not require compression var buffer = encoding.GetBytes(serialized); to.ContentLength = buffer.Length; // don't use chunked var ct = content.Headers.ContentType ?? new("application/json", encoding.HeaderName); ct.CharSet = encoding.HeaderName; to.ContentType = ct.ToString(); // always -> application/json; charset=utf-8 var contentEncoding = content.Headers.ContentEncoding; if (contentEncoding.Contains("br")) // Brotli compression { //var compressed = await CompressBrotliAsync(buffer, token); //var data = encoding.GetString(compressed); //await to.WriteAsync(data, encoding, token); // client app error: Decompression failed to.Headers.ContentEncoding = new("identity"); // don't compress with Brotli algo } else if (contentEncoding.Contains("zstd")) // Zstandard compression (Facebook) { to.Headers.ContentEncoding = new("identity"); // don't compress with Zstandard algo } else if (contentEncoding.Contains("deflate")) // Deflate compression { // Do nothing, because of impossibility to decompress third-party streams } else if (contentEncoding.Contains("gzip")) // GZip compression { to.Headers.ContentEncoding = new("identity"); // don't compress with GZip algo } return to.Body.WriteAsync(buffer, 0, buffer.Length, token); } // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.brotlistream?view=net-9.0 private static async Task CompressBrotliAsync(byte[] input, CancellationToken token) { using var output = new MemoryStream(input.Length); using var brotli = new BrotliStream(output, CompressionLevel.SmallestSize); await brotli.WriteAsync(input, token); await brotli.FlushAsync(token); return output.ToArray(); } // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.brotlistream?view=net-9.0 private static async Task DecompressBrotliAsync(Stream input, CancellationToken token) { using var output = new MemoryStream(); using var decompressor = new BrotliStream(input, CompressionMode.Decompress); await decompressor.CopyToAsync(output, token); return output.ToArray(); } // https://github.com/skbkontur/ZstdNet private static async Task DecompressZstandardAsync(Stream input, CancellationToken token) { using var output = new MemoryStream(); await using var decompression = new DecompressionStream(input); await decompression.CopyToAsync(output, token); return output.ToArray(); } // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream?view=net-9.0 private static async Task DecompressDeflateAsync(Stream input, CancellationToken token) { using var output = new MemoryStream(); using var decompressor = new DeflateStream(input, CompressionMode.Decompress); await decompressor.CopyToAsync(output, token); return output.ToArray(); } // https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.gzipstream?view=net-9.0 public static byte[] DecompressGZip(Stream input) { using var output = new MemoryStream(); using var decompressor = new GZipStream(input, CompressionMode.Decompress); decompressor.CopyTo(output); return output.ToArray(); } } ================================================ FILE: samples/Metadata/Models/PostsPlugin2.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class PostsPlugin2 { public string? name { get; set; } public int? age { get; set; } public string? city { get; set; } public bool? is_student { get; set; } public string[]? hobbies { get; set; } } ================================================ FILE: samples/Metadata/Models/TestDeflateResponse.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class TestDeflateResponse { public bool? deflated { get; set; } public Dictionary? headers { get; set; } public string? method { get; set; } } ================================================ FILE: samples/Metadata/Models/TestGZipResponse.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class TestGZipResponse { public bool? gzipped { get; set; } public Dictionary? headers { get; set; } public string? method { get; set; } } ================================================ FILE: samples/Metadata/Models/WeatherCurrent.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class WeatherCurrent { public long? last_updated_epoch { get; set; } public string? last_updated { get; set; } public float? temp_c { get; set; } public float? temp_f { get; set; } public int? is_day { get; set; } public WeatherCurrentCondition? condition { get; set; } public float? wind_mph { get; set; } public float? wind_kph { get; set; } public int? wind_degree { get; set; } public string? wind_dir { get; set; } public float? pressure_mb { get; set; } public float? pressure_in { get; set; } public float? precip_mm { get; set; } public float? precip_in { get; set; } public int? humidity { get; set; } public int? cloud { get; set; } public float? feelslike_c { get; set; } public float? feelslike_f { get; set; } public float? windchill_c { get; set; } public float? windchill_f { get; set; } public float? heatindex_c { get; set; } public float? heatindex_f { get; set; } public float? dewpoint_c { get; set; } public float? dewpoint_f { get; set; } public float? vis_km { get; set; } public float? vis_miles { get; set; } public float? uv { get; set; } public float? gust_mph { get; set; } public float? gust_kph { get; set; } } ================================================ FILE: samples/Metadata/Models/WeatherCurrentCondition.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class WeatherCurrentCondition { public string? text { get; set; } public string? icon { get; set; } public int? code { get; set; } } ================================================ FILE: samples/Metadata/Models/WeatherLocation.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class WeatherLocation { public string? name { get; set; } public string? region { get; set; } public string? country { get; set; } public float? lat { get; set; } public float? lon { get; set; } public string? tz_id { get; set; } public long? localtime_epoch { get; set; } public string? localtime { get; set; } } ================================================ FILE: samples/Metadata/Models/WeatherResponse.cs ================================================ namespace Ocelot.Samples.Metadata.Models; public class WeatherResponse { public WeatherLocation? location { get; set; } public WeatherCurrent? current { get; set; } } ================================================ FILE: samples/Metadata/MyMiddlewares.cs ================================================ using Ocelot.Logging; using Ocelot.Metadata; using Ocelot.Middleware; using Ocelot.Responder; using System.Text.Json; namespace Ocelot.Samples.Metadata; using Models; class MyMiddlewares { public static async Task PreErrorResponderMiddleware(HttpContext context, Func next) { // Get downstream response first await next.Invoke(); // ResponderMiddleware var loggerFactory = context.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger(); logger.LogDebug(() => $"My custom {nameof(PreErrorResponderMiddleware)} started"); var route = context.Items.DownstreamRoute(); var routeId = route.GetMetadata("route.id"); var routeName = route.GetMetadata("route.name", string.Empty); switch (routeId) { case 1: // ocelot-docs case 2: // ocelot-docs-BFF bool disabled = route.GetMetadata("disableMetadataJson"); break; case 3: // weather-current var cities = route.GetMetadata("cities"); var defaultCity = route.GetMetadata("cities.default"); var citiesUS = route.GetMetadata("cities.US"); var pathTemperatureCelsius = route.GetMetadata("temperature-celsius-path"); var dataResponse = route.GetMetadata("data/Response", new()); // TODO Refactor Ocelot Metadata helpers and Ocelot Core to support propagation of the JsonElement and JsonNode //var dataLocation = route.GetMetadataElement("stub-data/location", new JsonElement()); break; case 4: // ocelot-posts var id = route.GetMetadata("id"); var tags = route.GetMetadata("tags"); // Plugin 1 data var p1Enabled = route.GetMetadata("plugin1.enabled"); var p1Values = route.GetMetadata("plugin1.values"); var p1Param = route.GetMetadata("plugin1.param", "system-default-value"); var p1Param2 = route.GetMetadata("plugin1.param2"); // Plugin 2 data var p2Param1 = route.GetMetadata("plugin2/param1", "default-value"); var plugin2 = route.GetMetadata("plugin2/data", new()); break; case 5: // test-deflate var response1 = route.GetMetadata("data/Response", new()); break; case 6: // test-gzip var json = route.GetMetadata("data/Response", "{}"); // parse data manually var response2 = JsonSerializer.Deserialize(json); break; } // Reading global metadata var globalAppName = route.GetMetadata("app-name"); // Working with metadata // ... } public static async Task ResponderMiddleware(HttpContext context, Func next) { // Prepare services var responder = context.RequestServices.GetRequiredService(); var loggerFactory = context.RequestServices.GetRequiredService(); var logger = loggerFactory.CreateLogger(); var codeMapper = context.RequestServices.GetRequiredService(); logger.LogDebug(() => $"My custom {nameof(ResponderMiddleware)} started"); // Call original middleware Task @delegate(HttpContext c) => next(); var @base = new Responder.Middleware.ResponderMiddleware(@delegate, responder, loggerFactory, codeMapper); await @base.Invoke(context); // next.Invoke() } } ================================================ FILE: samples/Metadata/Ocelot.Samples.Metadata.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/Metadata https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Metadata/Program.cs ================================================ using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Responder; using Ocelot.Samples.Metadata; var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services .AddOcelot(builder.Configuration) .Services.RemoveAll() .TryAddSingleton(); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); var configuration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = MyMiddlewares.PreErrorResponderMiddleware, ResponderMiddleware = MyMiddlewares.ResponderMiddleware, // can be switched off/on }; await app.UseOcelot(configuration); await app.RunAsync(); ================================================ FILE: samples/Metadata/Properties/launchSettings.json ================================================ { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "http": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "http://localhost:5139", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "https://localhost:7252;http://localhost:5139", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: samples/Metadata/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/Metadata/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/Metadata/ocelot.json ================================================ { "Routes": [ { // route #1 aka 'ocelot-docs' "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/docs/{everything}", "DownstreamPathTemplate": "/en/latest/{everything}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ], "Metadata": { "route.id": 1, "route.name": "ocelot-docs", "disableMetadataJson": true // don't process json requested by scripts aka XHR } }, { // route #2 aka 'ocelot-docs-BFF' "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/_/{BFF}", "DownstreamPathTemplate": "/_/{BFF}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "ocelot.readthedocs.io", "Port": 443 } ], "Metadata": { "route.id": 2, "route.name": "ocelot-docs-BFF", "disableMetadataJson": true // don't process json requested by scripts aka XHR } }, { // route #3 aka 'weather-current' "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/weather/current/{city}", "DownstreamPathTemplate": "/v1/current.json?q={city}&key=4ea9a1d2aafe4e15bbd173615242312", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "api.weatherapi.com", "Port": 443 } ], "Metadata": { "route.id": 3, "route.name": "weather-current", "cities": "London, Paris, Madrid, Berlin, Rome, Prague, Warsaw, Minsk", "cities.default": "London", "cities.US": "New York, Los Angeles", "temperature-celsius-path": "Response/current/temp_c", "stub-data/location": { "name": "London", "region": "City of London, Greater London", "country": "United Kingdom", "lat": 51.5171, "lon": -0.1062, "tz_id": "Europe/London", "localtime_epoch": 1741269500, "localtime": "2025-03-06 13:58" }, "stub-data/current": { "last_updated_epoch": 1741268700, "last_updated": "2025-03-06 13:45", "temp_c": 17.1, "temp_f": 62.8, "is_day": 1, "condition": { "text": "Sunny", "icon": "//cdn.weatherapi.com/weather/64x64/day/113.png", "code": 1000 }, "wind_mph": 12.1, "wind_kph": 19.4, "wind_degree": 185, "wind_dir": "S", "pressure_mb": 1012.0, "pressure_in": 29.88, "precip_mm": 0.0, "precip_in": 0.0, "humidity": 45, "cloud": 0, "feelslike_c": 17.1, "feelslike_f": 62.8, "windchill_c": 12.8, "windchill_f": 55.0, "heatindex_c": 14.3, "heatindex_f": 57.7, "dewpoint_c": 4.4, "dewpoint_f": 39.9, "vis_km": 10.0, "vis_miles": 6.0, "uv": 2.3, "gust_mph": 14.1, "gust_kph": 22.7 }, "data/Response": "{\"location\":{\"name\":\"London\",\"region\":\"City of London, Greater London\",\"country\":\"United Kingdom\",\"lat\":51.5171,\"lon\":-0.1062,\"tz_id\":\"Europe/London\",\"localtime_epoch\":1741269500,\"localtime\":\"2025-03-06 13:58\"},\"current\":{\"last_updated_epoch\":1741268700,\"last_updated\":\"2025-03-06 13:45\",\"temp_c\":17.1,\"temp_f\":62.8,\"is_day\":1,\"condition\":{\"text\":\"Sunny\",\"icon\":\"//cdn.weatherapi.com/weather/64x64/day/113.png\",\"code\":1000},\"wind_mph\":12.1,\"wind_kph\":19.4,\"wind_degree\":185,\"wind_dir\":\"S\",\"pressure_mb\":1012.0,\"pressure_in\":29.88,\"precip_mm\":0.0,\"precip_in\":0.0,\"humidity\":45,\"cloud\":0,\"feelslike_c\":17.1,\"feelslike_f\":62.8,\"windchill_c\":12.8,\"windchill_f\":55.0,\"heatindex_c\":14.3,\"heatindex_f\":57.7,\"dewpoint_c\":4.4,\"dewpoint_f\":39.9,\"vis_km\":10.0,\"vis_miles\":6.0,\"uv\":2.3,\"gust_mph\":14.1,\"gust_kph\":22.7}}" } // end of metadata }, // end of route #3 aka 'weather-current' { // route #4 aka 'ocelot-posts' "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/ocelot/posts/{id}", "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ], "Metadata": { "route.id": 4, "route.name": "ocelot-posts", "id": "FindPost", "tags": "tag1, tag2, area1, area2, func1", "plugin1.enabled": "true", "plugin1.values": "[1, 2, 3, 4, 5]", "plugin1.param": "value2", "plugin1.param2": "123", "plugin2/param1": "overwritten-value", "plugin2/data": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}" } // end of metadata }, // end of route #4 aka 'ocelot-posts' { // route #5 "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/test/deflate", "DownstreamPathTemplate": "/deflate", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "postman-echo.com", "Port": 443 } ], "Metadata": { "route.id": 5, "route.name": "test-deflated", "data/Response": "{\"deflated\": true,\"headers\":{\"host\":\"postman-echo.com\",\"x-request-start\":\"t1741445435.299\",\"connection\":\"close\",\"x-forwarded-proto\":\"https\",\"x-forwarded-port\":\"443\",\"x-amzn-trace-id\":\"Root=1-67cc593b-697296304a4cdd9f25ff5b1a\",\"accept\":\"*/*\",\"user-agent\":\"PostmanRuntime/7.43.0\",\"accept-encoding\":\"gzip, deflate, br\",\"cookie\":\"sails.sid=s%3Al8bqLyxyBLVEzTvXkXmqzKy-oxu-Ofix.n5%2F%2FJgIegPYVSpaNLkI3oaLMBTeCG7rSDP95Tvzexx0\",\"oc-data\":\"deflate, gzip\",\"postman-token\":\"0db2b611-6e61-4c07-80cf-72a25b5dbff9\",\"traceparent\":\"00-d293c56e9acc1b154d470acf93f605b6-677ec7e38b6a8f1f-00\"},\"method\":\"GET\"}" } }, { // route #6 "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/test/gzip", "DownstreamPathTemplate": "/gzip", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "postman-echo.com", "Port": 443 } ], "Metadata": { "route.id": 6, "route.name": "test-gzip", "data/Response": "{\"gzipped\":true,\"headers\":{\"host\":\"postman-echo.com\",\"x-request-start\":\"t1741444484.025\",\"connection\":\"close\",\"x-forwarded-proto\":\"https\",\"x-forwarded-port\":\"443\",\"accept\":\"*/*\",\"user-agent\":\"PostmanRuntime/7.43.0\",\"accept-encoding\":\"gzip, deflate, br\",\"oc-data\":\"deflate, gzip\",\"postman-token\":\"50831f03-e4ed-4970-bd33-69720171d016\",\"traceparent\":\"00-6f958a4ad549dd11fc08b5a411277688-40a2fe22bf119682-00\"},\"method\":\"GET\"}" } } // end of route #6 ], "GlobalConfiguration": { "BaseUrl": "http://localhost:5139", "Metadata": { "app-name": "Ocelot Metadata sample" }, "MetadataOptions": { "CurrentCulture": "en-GB" } } } ================================================ FILE: samples/Metadata/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "ZstdNet": { "type": "Direct", "requested": "[1.5.7, )", "resolved": "1.5.7", "contentHash": "BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "ZstdNet": { "type": "Direct", "requested": "[1.5.7, )", "resolved": "1.5.7", "contentHash": "BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "ZstdNet": { "type": "Direct", "requested": "[1.5.7, )", "resolved": "1.5.7", "contentHash": "BpQLIV4HtklLEkCkCjepKTxxy/dcBNSrEfh5LPcIjpPaN2Cmuwg1TUVCWhn5zGn4z3Aur6V9taWdvn+BJzmEhw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/OpenTracing/Ocelot.Samples.OpenTracing.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 25.0.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/OpenTracing https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/OpenTracing/Program.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Samples.Web; using Ocelot.Tracing.OpenTracing; using OpenTracing.Util; //_ = OcelotHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services .AddSingleton(serviceProvider => { var loggerFactory = serviceProvider.GetService(); var config = new Jaeger.Configuration(builder.Environment.ApplicationName, loggerFactory); var tracer = config.GetTracer(); GlobalTracer.Register(tracer); return tracer; }) .AddOcelot(builder.Configuration) .AddOpenTracing(); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); ================================================ FILE: samples/OpenTracing/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5562" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7784;http://localhost:5562" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7784/posts/1", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7784;http://localhost:5562" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62562/", "sslPort": 44362 } } } ================================================ FILE: samples/OpenTracing/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/OpenTracing/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/OpenTracing/ocelot.json ================================================ { "Routes": [ { "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/posts/{id}", "DownstreamPathTemplate": "/todos/{id}", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ], "HttpHandlerOptions": { "UseTracing": true } } ], "GlobalConfiguration": { "BaseUrl": "https://localhost:7784" } } ================================================ FILE: samples/OpenTracing/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Jaeger": { "type": "Direct", "requested": "[1.0.3, )", "resolved": "1.0.3", "contentHash": "y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==", "dependencies": { "Jaeger.Core": "1.0.3", "Jaeger.Senders.Thrift": "1.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", "requested": "[10.0.3, )", "resolved": "10.0.3", "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" }, "Ocelot.Tracing.OpenTracing": { "type": "Direct", "requested": "[25.0.0-beta.2, )", "resolved": "25.0.0-beta.2", "contentHash": "C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==", "dependencies": { "Ocelot": "25.0.0-beta.1", "OpenTracing": "0.12.1" } }, "ApacheThrift": { "type": "Transitive", "resolved": "0.14.1", "contentHash": "WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==", "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Console": "3.1.0", "Microsoft.Extensions.Logging.Debug": "3.1.0", "System.Net.Http.WinHttpHandler": "4.7.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Jaeger.Communication.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==", "dependencies": { "ApacheThrift": "0.14.1" } }, "Jaeger.Core": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==", "dependencies": { "Microsoft.Extensions.Configuration.EnvironmentVariables": "2.2.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Newtonsoft.Json": "12.0.1", "OpenTracing": "0.12.1" } }, "Jaeger.Senders.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==", "dependencies": { "Jaeger.Communication.Thrift": "1.0.3", "Jaeger.Core": "1.0.3" } }, "Microsoft.AspNetCore.Http.Abstractions": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", "dependencies": { "Microsoft.AspNetCore.Http.Features": "2.2.0" } }, "Microsoft.AspNetCore.Http.Features": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", "dependencies": { "Microsoft.Extensions.Primitives": "2.2.0" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.3", "contentHash": "IHsqsECi1N2FJ0RmV73Cmp6qusu4vGBhUuWJFyJAC/LekFdwSa5zacZE80Sd8M2fD9ZXgEaA32y5qcj3jh3wlQ==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.3", "contentHash": "LLPdY4BEQ94be1eiXYyeFhcern4jOoMgIKLmfFpEvXafbcsSZtCXk0yT6seoyCJsh1vrdTVKYbLH+3b6/actfg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.3" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.3", "contentHash": "gnCyVHEYeI3oeK1pig6F3ckmTKew5wJO5V70vj7rKp4KOoPUijGcigsaFdJfj5HZBXMmYuJpBiaWCHauXJ0GLw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.3", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==", "dependencies": { "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==", "dependencies": { "Microsoft.Extensions.Configuration": "3.1.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==", "dependencies": { "Microsoft.Extensions.Configuration": "2.2.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection": "3.1.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Configuration": "3.1.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Ocelot": { "type": "Transitive", "resolved": "25.0.0-beta.1", "contentHash": "uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==", "dependencies": { "FluentValidation": "12.1.1", "IPAddressRange": "6.3.0", "Microsoft.AspNetCore.MiddlewareAnalysis": "10.0.3", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "10.0.3", "Microsoft.Extensions.DiagnosticAdapter": "3.1.32" } }, "OpenTracing": { "type": "Transitive", "resolved": "0.12.1", "contentHash": "8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==" }, "System.Net.Http.WinHttpHandler": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==" }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Jaeger": { "type": "Direct", "requested": "[1.0.3, )", "resolved": "1.0.3", "contentHash": "y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==", "dependencies": { "Jaeger.Core": "1.0.3", "Jaeger.Senders.Thrift": "1.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", "requested": "[10.0.3, )", "resolved": "10.0.3", "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" }, "Ocelot.Tracing.OpenTracing": { "type": "Direct", "requested": "[25.0.0-beta.2, )", "resolved": "25.0.0-beta.2", "contentHash": "C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==", "dependencies": { "Ocelot": "25.0.0-beta.1", "OpenTracing": "0.12.1" } }, "ApacheThrift": { "type": "Transitive", "resolved": "0.14.1", "contentHash": "WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==", "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Console": "3.1.0", "Microsoft.Extensions.Logging.Debug": "3.1.0", "System.Net.Http.WinHttpHandler": "4.7.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Jaeger.Communication.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==", "dependencies": { "ApacheThrift": "0.14.1" } }, "Jaeger.Core": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==", "dependencies": { "Microsoft.Extensions.Configuration.EnvironmentVariables": "2.2.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Newtonsoft.Json": "12.0.1", "OpenTracing": "0.12.1" } }, "Jaeger.Senders.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==", "dependencies": { "Jaeger.Communication.Thrift": "1.0.3", "Jaeger.Core": "1.0.3" } }, "Microsoft.AspNetCore.Http.Abstractions": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", "dependencies": { "Microsoft.AspNetCore.Http.Features": "2.2.0" } }, "Microsoft.AspNetCore.Http.Features": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", "dependencies": { "Microsoft.Extensions.Primitives": "2.2.0" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.24", "contentHash": "fup+Ya6mN58877F6eKzR8jrMe2fCRQ/Bl3pA/23DtX+1R2eWdDTrZGYOGDrnt2aWN5VgLSlxc7APFgXiK57l8w==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.24", "contentHash": "qb0pE7PBNUiIVtFleAZ4gq7KLQuPGOjAhA4TbC/NLLpsP1WXJtDXcqTBdta6iJQBDtmeWVSijy6KyX0hZcr/WQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.24", "contentHash": "TvbyHnoETdT71rTFlBLUJ6pOCu1nQf4Y4dkt/g2lEqKN2+CSraY2rUPyYrpPeH5oopSQGrDNFO3pVCBrfbjxjg==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.24", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==", "dependencies": { "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==", "dependencies": { "Microsoft.Extensions.Configuration": "3.1.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==", "dependencies": { "Microsoft.Extensions.Configuration": "2.2.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection": "3.1.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Configuration": "3.1.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Ocelot": { "type": "Transitive", "resolved": "25.0.0-beta.1", "contentHash": "uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==", "dependencies": { "FluentValidation": "12.1.1", "IPAddressRange": "6.3.0", "Microsoft.AspNetCore.MiddlewareAnalysis": "8.0.24", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.24", "Microsoft.Extensions.DiagnosticAdapter": "3.1.32" } }, "OpenTracing": { "type": "Transitive", "resolved": "0.12.1", "contentHash": "8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==" }, "System.Net.Http.WinHttpHandler": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==" }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Jaeger": { "type": "Direct", "requested": "[1.0.3, )", "resolved": "1.0.3", "contentHash": "y7BUOpX6K+iKcY4j+VLgX4iUSejgPL5iCp5H+K06N8YFS70xOc4Si7l17B32+2yKPAd+ROHKSgBG16OKcupH5g==", "dependencies": { "Jaeger.Core": "1.0.3", "Jaeger.Senders.Thrift": "1.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Direct", "requested": "[10.0.3, )", "resolved": "10.0.3", "contentHash": "bwGMrRcAMWx2s/RDgja97p27rxSz2pEQW0+rX5cWAUWVETVJ/eyxGfjAl8vuG5a+lckWmPIE+vcuaZNVB5YDdw==" }, "Ocelot.Tracing.OpenTracing": { "type": "Direct", "requested": "[25.0.0-beta.2, )", "resolved": "25.0.0-beta.2", "contentHash": "C2pnNWrpzsWhj45rdqgtGKYlVz059LnP4NZe5VWoE6tYnQI3dmIpZGlVPu9cgf6psxMrNSx9qg00Zrws7RH1Lg==", "dependencies": { "Ocelot": "25.0.0-beta.1", "OpenTracing": "0.12.1" } }, "ApacheThrift": { "type": "Transitive", "resolved": "0.14.1", "contentHash": "WQDdwNKZ8BeKtonbb8lxu2fVg9CHNbchJ8SGKtadVmxZC5l8wzhfKitaIQLuawch+8HhcAhuIf1FqiLYrlO3Bg==", "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Console": "3.1.0", "Microsoft.Extensions.Logging.Debug": "3.1.0", "System.Net.Http.WinHttpHandler": "4.7.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Jaeger.Communication.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "LwWqWk37RXOpUsdj1PnYZKruBvyVWeP2cY3jcl+oxIghV2f8tvwOmOnJgUVC/xerMnp1MfQVGoHaud20x1ioFw==", "dependencies": { "ApacheThrift": "0.14.1" } }, "Jaeger.Core": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "GEqd4C0sh4+n1fxeOIXKjsMo22j8QJokinFnkZO+ikQrcGz6ulh8DJ8J2NibrThPFN/6O/gqLtNM89y1p30ZOQ==", "dependencies": { "Microsoft.Extensions.Configuration.EnvironmentVariables": "2.2.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Newtonsoft.Json": "12.0.1", "OpenTracing": "0.12.1" } }, "Jaeger.Senders.Thrift": { "type": "Transitive", "resolved": "1.0.3", "contentHash": "3SJ2QhJW0sA+F85wnJOX329KRjdxeR204IjPaUJNzya03ezQ35k+132KT23ISeq1QCvYSRz3sDZrFuNCqlnMhw==", "dependencies": { "Jaeger.Communication.Thrift": "1.0.3", "Jaeger.Core": "1.0.3" } }, "Microsoft.AspNetCore.Http.Abstractions": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==", "dependencies": { "Microsoft.AspNetCore.Http.Features": "2.2.0" } }, "Microsoft.AspNetCore.Http.Features": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==", "dependencies": { "Microsoft.Extensions.Primitives": "2.2.0" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.13", "contentHash": "97bu/KDJKJypkpQb0hq2YDxFy4f30g/4Wmk2I8XTxDvaXbGL2UcLQGdrLWAIW+NlEAFI+Zrps1Oe92uO26vRLQ==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.13", "contentHash": "qfh2o5iXQvummtKgaui21dbmOjhBoQfwscxgfxDUUlvNa+Qj6hMwqQUOLQ+/oG+8caUDkdSWzMdcu8Z79UT4GQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.13" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.13", "contentHash": "hH3hfEYrm97r5+11BeezwT4LmDvgGPzq3GvtChhCV9AA2igWPkzA5E0ZmtPWdU9W124QZmceMztDZs68xgkHOw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.13", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "Lu41BWNmwhKr6LgyQvcYBOge0pPvmiaK8R5UHXX4//wBhonJyWcT2OK1mqYfEM5G7pTf31fPrpIHOT6sN7EGOA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "ESz6bVoDQX7sgWdKHF6G9Pq672T8k+19AFb/txDXwdz7MoqaNQj2/in3agm/3qae9V+WvQZH86LLTNVo0it8vQ==", "dependencies": { "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "o9eELDBfNkR7sUtYysFZ1Q7BQ1mYt27DMkups/3vu7xgPyOpMD+iAfrBZFzUXT2iw0fmFb8s1gfNBZS+IgjKdQ==", "dependencies": { "Microsoft.Extensions.Configuration": "3.1.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "2.2.0", "contentHash": "gIqt9PkKO01hZ0zmHnWrZ1E45MDreZTVoyDbL1kMWKtDgxxWTJpYtESTEcgpvR1uB1iex1zKGYzJpOMgmuP5TQ==", "dependencies": { "Microsoft.Extensions.Configuration": "2.2.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "KVkv3aF2MQpmGFRh4xRx2CNbc2sjDFk+lH4ySrjWSOS+XoY1Xc+sJphw3N0iYOpoeCCq8976ceVYDH8sdx2qIQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "P+8sKQ8L4ooL79sxxqwFPxGGC3aBrUDLB/dZqhs4J0XjTyrkeeyJQ4D4nzJB6OnAhy78HIIgQ/RbD6upOXLynw==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection": "3.1.0", "Microsoft.Extensions.Logging.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "jjo4YXRx6MIpv6DiRxJjSpl+sPP0+5VW0clMEdLyIAz44PPwrDTFrd5PZckIxIXl1kKZ2KK6IL2nkt0+ug2MQg==" }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "yW3nIoNM3T5iZg8bRViiCN4+vIU/02l+mlWSvKqWnr0Fd5Uk1zKdT9jBWKEcJhRIWKVWWSpFWXnM5yWoIAy1Eg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "lkqVlnQqfqr8nmrpy7EnISXy192NEaIb0Xh4zaO8XS/QMiC3RqOAxxeq89JPSmjw+YfecwcikV8UB+NacdgRDg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Logging.Configuration": "3.1.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "pTuUb46wKuZD2nuvzmlHQfbm9sUAiOg1x1pe1fzxDHaocXaZQDjKGYYZifnTLeJk35OSi8ouEIcbuKLVF16yfg==", "dependencies": { "Microsoft.Extensions.Logging": "3.1.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "9b6JHY7TAXrSfZ6EEGf+j8XnqKIiMPErfmaNXhJYSCb+BUW2H4RtzkNJvwLJzwgzqBP0wtTjyA6Uw4BPPdmkMw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Primitives": "3.1.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "tx6gMKE3rDspA1YZT8SlQJmyt1BaBSl6mNjB3g0ZO6m3NnoavCifXkGeBuDk9Ae4XjW8C+dty52p+0u38jPRIQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "3.1.0", "Microsoft.Extensions.Configuration.Binder": "3.1.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "LEKAnX7lhUhSoIc2XraCTK3M4IU/LdVUzCe464Sa4+7F4ZJuXHHRzZli2mDbiT4xzAZhgqXbvfnb5+CNDcQFfg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Ocelot": { "type": "Transitive", "resolved": "25.0.0-beta.1", "contentHash": "uefe0tEhGeB9hQ8lSQxYn+oj6UfYD9RWR6A48+Byxw2PGZCS0wCL8z8cfF/O/tnrKCmb8Ip511qZV/dhUr0I1w==", "dependencies": { "FluentValidation": "12.1.1", "IPAddressRange": "6.3.0", "Microsoft.AspNetCore.MiddlewareAnalysis": "9.0.13", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "9.0.13", "Microsoft.Extensions.DiagnosticAdapter": "3.1.32" } }, "OpenTracing": { "type": "Transitive", "resolved": "0.12.1", "contentHash": "8i/Vnx/lbWzqqJ6J5lofguT4wBS99rfqKujWrFrTGAclQBZ5h1CgBlzGOTqsNjmMsxSTLpC+Ns6/f1RB0c4O/g==" }, "System.Net.Http.WinHttpHandler": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "fEsjB8hXnH2xqGEF9NbN5EWj2JpHyqIw/VYbsrSNU0ri+ZBWLACLS9iKy8amjN5fw6cZRFxIG10j+osmQ1dRpw==" }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/ServiceDiscovery/.dockerignore ================================================ **/.classpath **/.dockerignore **/.env **/.git **/.gitignore **/.project **/.settings **/.toolstarget **/.vs **/.vscode **/*.*proj.user **/*.dbmdl **/*.jfm **/azds.yaml **/bin **/charts **/docker-compose* **/Dockerfile* **/node_modules **/npm-debug.log **/obj **/secrets.dev.yaml **/values.dev.yaml LICENSE README.md ================================================ FILE: samples/ServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Leon Lucardie, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/ServiceDiscovery/ApiGateway/Program.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery; using Ocelot.Samples.Web; using Ocelot.ServiceDiscovery; _ = OcelotHostBuilder.Create(args); var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); // Perform initialization from application configuration or hardcode/choose the best option. bool easyWay = true; if (easyWay) { // Design #1: Define a custom finder delegate to instantiate a custom provider // under the default factory (ServiceDiscoveryProviderFactory). builder.Services .AddSingleton((serviceProvider, config, downstreamRoute) => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute)); } else { // Design #2: Abstract from the default factory (ServiceDiscoveryProviderFactory) and FinderDelegate, // and create your own factory by implementing the IServiceDiscoveryProviderFactory interface. builder.Services .RemoveAll() .AddSingleton(); // This will not be called but is required for internal validators. It's also a handy workaround. builder.Services .AddSingleton((serviceProvider, config, downstreamRoute) => null); } builder.Services .AddOcelot(builder.Configuration); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); await app.UseOcelot(); await app.RunAsync(); ================================================ FILE: samples/ServiceDiscovery/ApiGateway/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "categories", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5563" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "categories", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7785;http://localhost:5563" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "categories", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7785/categories", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7785;http://localhost:5563" }, "distributionName": "" } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62563/", "sslPort": 44363 } } } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs ================================================ using Ocelot.Configuration; using Ocelot.Metadata; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery; public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider { private readonly IServiceProvider _serviceProvider; private readonly ServiceProviderConfiguration _config; private readonly DownstreamRoute _downstreamRoute; public MyServiceDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute) { _serviceProvider = serviceProvider; _config = config; _downstreamRoute = downstreamRoute; } public Task> GetAsync() { // Returns a list of service(s) that match the downstream route passed to the provider var services = new List(); // Apply configuration checks // ... if (_config.Host) if (_downstreamRoute.ServiceName.Equals("downstream-service")) { var instance = _downstreamRoute.GetMetadata("instance"); var srv = instance.Split(':'); //For this example we simply do a manual match to a single service var service = new Service( name: "downstream-service", hostAndPort: new ServiceHostAndPort(srv[0], int.Parse(srv[1])), id: "downstream-service-1", version: "1.0", tags: new string[] { "downstream", "hardcoded" } ); services.Add(service); } return Task.FromResult(services); } } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs ================================================ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Samples.ServiceDiscovery.ApiGateway.ServiceDiscovery; public class MyServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { private readonly IOcelotLoggerFactory _factory; private readonly IServiceProvider _provider; public MyServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) { _factory = factory; _provider = provider; } public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { // Apply configuration checks // ... // Create the provider based on configuration and route info var provider = new MyServiceDiscoveryProvider(_provider, serviceConfig, route); return new OkResponse(provider); } } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/ocelot.json ================================================ { "Routes": [ { "ServiceName": "downstream-service", "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/categories", "DownstreamPathTemplate": "/categories", "DownstreamScheme": "https", "FileCacheOptions": { "TtlSeconds": 15 }, "Metadata": { "instance": "localhost:7786" } }, { "ServiceName": "downstream-service", "UpstreamHttpMethod": [ "Get" ], "UpstreamPathTemplate": "/health", "DownstreamPathTemplate": "/health", "DownstreamScheme": "https", "Metadata": { "instance": "localhost:7786" } } ], "GlobalConfiguration": { "RequestIdKey": "OcRequestId", "AdministrationPath": "/administration", "ServiceDiscoveryProvider": { "Type": "MyServiceDiscoveryProvider" } } } ================================================ FILE: samples/ServiceDiscovery/ApiGateway/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs ================================================ namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers; [ApiController] [Route("[controller]")] public class CategoriesController : ControllerBase { // GET /categories [HttpGet] public IEnumerable Get() { var random = new Random(); int max = DateTime.Now.Second; int length = random.Next(max); var categories = new List(length); for (int i = 0; i < length; i++) { max = DateTime.Now.Millisecond < 3 ? DateTime.Now.Millisecond + 3 : DateTime.Now.Millisecond; categories.Add("category" + random.Next(max)); } return categories; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Controllers/HealthController.cs ================================================ using System.Reflection; namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers; using Models; [ApiController] [Route("[controller]")] public class HealthController : ControllerBase { private static readonly DateTime startedAt = DateTime.Now; private static readonly Assembly assembly = Assembly.GetExecutingAssembly(); // GET /health [HttpGet] [Route("/health", Name = nameof(Health))] public MicroserviceResult Health() { // Analyze integrated services, get their health and return the Health flag bool isHealthy = true; // Get the link of the first action of current microservice workflow var link = Url.RouteUrl(routeName: "GetWeatherForecast", values: null, protocol: Request.Scheme); return new HealthResult { Healthy = isHealthy, Next = new Uri(link), }; } // GET /ready [HttpGet] [Route("/ready", Name = nameof(Ready))] public MicroserviceResult Ready() { var asmName = assembly.GetName(); //var link = Url.Action(action: nameof(Health), controller: nameof(Health), values: null, protocol: Request.Scheme); //var link = Url.RouteUrl(routeName: nameof(Health), values: null, protocol: Request.Scheme); var link = Url.Link(nameof(Health), null); return new ReadyResult { ServiceName = asmName.Name, ServiceVersion = asmName.Version.ToString(), StartedAt = startedAt, Next = new Uri(link), }; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs ================================================ namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Controllers; using Models; [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger _logger; public WeatherForecastController(ILogger logger) { _logger = logger; } [HttpGet(Name = "GetWeatherForecast")] public IEnumerable Get() { return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)), TemperatureC = Random.Shared.Next(-20, 55), Summary = Summaries[Random.Shared.Next(Summaries.Length)] }) .ToArray(); } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Models/HealthResult.cs ================================================ namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; public class HealthResult : MicroserviceResult { public bool Healthy { get; set; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs ================================================ namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; public class MicroserviceResult { public Uri Next { get; set; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Models/ReadyResult.cs ================================================ using System; namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; public class ReadyResult : MicroserviceResult { public string ServiceName { get; set; } public string ServiceVersion { get; set; } public DateTime StartedAt { get; set; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Models/WeatherForecast.cs ================================================ namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); public string Summary { get; set; } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj ================================================  net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Leon Lucardie, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceDiscovery README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Program.cs ================================================ global using Microsoft.AspNetCore.Mvc; using System.Text.Json; using System.Text.Json.Serialization; [assembly: ApiController] var builder = WebApplication.CreateBuilder(args); builder.Services // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle .AddEndpointsApiExplorer() .AddSwaggerGen() .AddHttpClient() // to keep performance of HTTP Client high .AddControllers() .AddJsonOptions(options => { options.AllowInputFormatterExceptionMessages = true; var jOptions = options.JsonSerializerOptions; jOptions.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase, true)); jOptions.PropertyNameCaseInsensitive = true; jOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; }); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); app.UseDeveloperExceptionPage(); } else { app.UseHttpsRedirection(); } app.UseRouting(); app.MapControllers(); app.Run(); ================================================ FILE: samples/ServiceDiscovery/DownstreamService/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5564" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7786;http://localhost:5564" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7786/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7786;http://localhost:5564" }, "distributionName": "" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "publishAllPorts": true, "useSSL": true } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62564/", "sslPort": 44364 } } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/ServiceDiscovery/DownstreamService/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "10.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "NCWCGiwRwje8773yzPQhvucYnnfeR+ZoB1VRIrIMp4uaeUNw7jvEPHij3HIbwCDuNCrNcphA00KSAR9yD9qmbg==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "8.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "jDM3a95WerM8g6IcMiBXq1qRS9dqmEUpgnCk2DeMWpPkYtp1ia+CkXabOnK93JmhVlUmv8l9WMPsCSUm+WqkIA==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Swashbuckle.AspNetCore": { "type": "Direct", "requested": "[10.1.5, )", "resolved": "10.1.5", "contentHash": "/eNk9z/8quXhDX14o3XLbwAX/84uIWSbiUD7cI/UrQnoBMOiyAtzKxNEJUtf/TyxjFpcXxE9FAfLvtbNpxHBSg==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "9.0.0", "Swashbuckle.AspNetCore.Swagger": "10.1.5", "Swashbuckle.AspNetCore.SwaggerGen": "10.1.5", "Swashbuckle.AspNetCore.SwaggerUI": "10.1.5" } }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "9.0.0", "contentHash": "1Kzzf7pRey40VaUkHN9/uWxrKVkLu2AQjt+GVeeKLLpiEHAJ1xZRsLSh4ZZYEnyS7Kt2OBOPmsXNdU+wbcOl5w==" }, "Microsoft.OpenApi": { "type": "Transitive", "resolved": "2.4.1", "contentHash": "u7QhXCISMQuab3flasb1hoaiERmUqyWsW7tmQODyILoQ7mJV5IRGM+2KKZYo0QUfC13evEOcHAb6TPWgqEQtrw==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "s4Mct6+Ob0LK9vYVaZcYi/RFFCOEJNjf6nJ5ZPoxtpdFSlzR6i9AHI7Vl44obX8cynRxJW7prA1IUabkiXolFg==", "dependencies": { "Microsoft.OpenApi": "2.4.1" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "ysQIRgqnx4Vb/9+r3xnEAiaxYmiBHO8jTg7ACaCh+R3Sn+ZKCWKD6nyu0ph3okP91wFSh/6LgccjeLUaQHV+ZA==", "dependencies": { "Swashbuckle.AspNetCore.Swagger": "10.1.5" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", "resolved": "10.1.5", "contentHash": "tQWVKNJWW7lf6S0bv22+7yfxK5IKzvsMeueF4XHSziBfREhLKt42OKzi6/1nINmyGlM4hGbR8aSMg72dLLVBLw==" }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/ServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{411000B6-ACB0-4323-8FE4-A4DE0E590ACB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Debug|Any CPU.Build.0 = Debug|Any CPU {411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Release|Any CPU.ActiveCfg = Release|Any CPU {411000B6-ACB0-4323-8FE4-A4DE0E590ACB}.Release|Any CPU.Build.0 = Release|Any CPU {4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F302EAE-1C67-47CA-ACCE-D05DF00AAAC1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2C604707-2EA1-4CCF-A89C-22B613052C8D} EndGlobalSection EndGlobal ================================================ FILE: samples/ServiceDiscovery/README.md ================================================ # Ocelot Service Discovery Custom Provider > An example how to build custom service discovery in Ocelot.
> **Documentation**: [Service Discovery](../../docs/features/servicediscovery.rst) > [Custom Providers](../../docs/features/servicediscovery.rst#custom-providers) This sample constains a basic setup using a custom service discovery provider.
## Instructions ### 1. Run Downstream Service app ```shell cd ./DownstreamService/ dotnet run ``` Leave the service running. ### 2. Run API Gateway app ```shell cd ./ApiGateway/ dotnet run ``` Leave the gateway running. ### 3. Make a HTTP request To the URL: http://localhost:5000/categories
You should get the following response: ```json { [ "category1", "category2" ] } ``` ================================================ FILE: samples/ServiceFabric/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Service fabric OcelotApplicationApiGatewayPkg/Code OcelotApplication/OcelotApplicationApiGatewayPkg/Code/appsettings.json OcelotApplication/OcelotApplicationApiGatewayPkg/Code/ocelot.json OcelotApplication/OcelotApplicationApiGatewayPkg/Code/runtimes/ OcelotApplicationServicePkg/Code OcelotApplication/OcelotApplicationApiGatewayPkg/Code/web.config OcelotApplication/OcelotApplicationServicePkg/Code/runtimes/ !entryPoint.cmd !entryPoint.sh # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # JetBrains Rider .idea/ *.sln.iml # Dotnet generated files *.dll *.pdb *.deps.json *.runtimeconfig.json ================================================ FILE: samples/ServiceFabric/ApiGateway/Ocelot.Samples.ServiceFabric.ApiGateway.csproj ================================================  Stateless Web Service for Stateful OcelotApplicationApiGateway App Any CPU;x64 net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/ServiceFabric/ApiGateway/OcelotApplicationApiGateway.cs ================================================ // ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ using Microsoft.ServiceFabric.Services.Communication.Runtime; using Microsoft.ServiceFabric.Services.Runtime; using System.Fabric; namespace Ocelot.Samples.ServiceFabric.ApiGateway; /// Service that handles front-end web requests and acts as a proxy to the back-end data for the UI web page. /// It is a stateless service that hosts a Web API application on OWIN. internal sealed class OcelotServiceWebService : StatelessService { public OcelotServiceWebService(StatelessServiceContext context) : base(context) { } protected override IEnumerable CreateServiceInstanceListeners() { return new[] { new ServiceInstanceListener( initparams => new WebCommunicationListener(string.Empty, initparams), "OcelotServiceWebListener") }; } } ================================================ FILE: samples/ServiceFabric/ApiGateway/Program.cs ================================================ using Microsoft.ServiceFabric.Services.Runtime; using Ocelot.Samples.ServiceFabric.ApiGateway; using System.Diagnostics.Tracing; // The service host is the executable that hosts the Service instances. // Create Service Fabric runtime and register the service type. try { //Creating a new event listener to redirect the traces to a file var listener = new ServiceEventListener("OcelotApplicationApiGateway"); listener.EnableEvents(ServiceEventSource.Current, EventLevel.LogAlways, EventKeywords.All); // The ServiceManifest.XML file defines one or more service type names. // Registering a service maps a service type name to a .NET type. // When Service Fabric creates an instance of this service type, // an instance of the class is created in this host process. await ServiceRuntime.RegisterServiceAsync("OcelotApplicationApiGatewayType", context => new OcelotServiceWebService(context)); // Prevents this host process from terminating so services keep running. Thread.Sleep(Timeout.Infinite); } catch (Exception ex) { ServiceEventSource.Current.ServiceHostInitializationFailed(ex); throw; } ================================================ FILE: samples/ServiceFabric/ApiGateway/Properties/launchSettings.json ================================================ { "profiles": { "http": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://localhost:5565" }, "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7787;http://localhost:5565" }, "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "launchUrl": "swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "WSL": { "commandName": "WSL2", "launchBrowser": true, "launchUrl": "https://localhost:7787/swagger", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", "ASPNETCORE_URLS": "https://localhost:7787;http://localhost:5565" }, "distributionName": "" }, "Docker": { "commandName": "Docker", "launchBrowser": true, "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "publishAllPorts": true, "useSSL": true } }, "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:62565/", "sslPort": 44365 } } } ================================================ FILE: samples/ServiceFabric/ApiGateway/ServiceEventListener.cs ================================================ // ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ using System.Diagnostics.Tracing; using System.Globalization; using System.Text; namespace Ocelot.Samples.ServiceFabric.ApiGateway; /// /// ServiceEventListener is a class which listens to the eventsources registered and redirects the traces to a file /// Note that this class serves as a template to EventListener class and redirects the logs to /tmp/{appnameyyyyMMddHHmmssffff}. /// You can extend the functionality by writing your code to implement rolling logs for the logs written through this class. /// You can also write your custom listener class and handle the registered evestsources accordingly. /// internal class ServiceEventListener : EventListener { private readonly string _fileName; private readonly string _filepath = Path.GetTempPath(); public ServiceEventListener(string appName) { _fileName = appName + DateTime.Now.ToString("yyyyMMddHHmmssffff"); } /// /// We override this method to get a callback on every event we subscribed to with EnableEvents /// /// The event arguments that describe the event. protected override void OnEventWritten(EventWrittenEventArgs eventData) { using var writer = new StreamWriter(new FileStream(_filepath + _fileName, FileMode.Append)); // report all event information writer.Write(" {0} ", Write( eventData.Task.ToString(), eventData.EventName!, eventData.EventId.ToString(), eventData.Level)); if (eventData.Message != null) { writer.WriteLine(string.Format(CultureInfo.InvariantCulture, eventData.Message, eventData.Payload!.ToArray())); } } private static String Write(string taskName, string eventName, string id, EventLevel level) { var output = new StringBuilder(); var now = DateTime.UtcNow; output.Append(now.ToString("yyyy/MM/dd-HH:mm:ss.fff", CultureInfo.InvariantCulture)); output.Append(','); output.Append(ConvertLevelToString(level)); output.Append(','); output.Append(taskName); if (!string.IsNullOrEmpty(eventName)) { output.Append('.'); output.Append(eventName); } if (!string.IsNullOrEmpty(id)) { output.Append('@'); output.Append(id); } output.Append(','); return output.ToString(); } private static string ConvertLevelToString(EventLevel level) { return level switch { EventLevel.Informational => "Info", _ => level.ToString(), }; } } ================================================ FILE: samples/ServiceFabric/ApiGateway/ServiceEventSource.cs ================================================ // ------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License (MIT). See License.txt in the repo root for license information. // ------------------------------------------------------------ using System.Diagnostics.Tracing; namespace Ocelot.Samples.ServiceFabric.ApiGateway; /// /// Implements methods for logging service related events. /// public class ServiceEventSource : EventSource { public static ServiceEventSource Current = new(); // Define an instance method for each event you want to record and apply an [Event] attribute to it. // The method name is the name of the event. // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). // Put [NonEvent] attribute on all methods that do not define an event. // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx [NonEvent] public void Message(string message, params object[] args) { if (IsEnabled()) { var finalMessage = string.Format(message, args); Message(finalMessage); } } private const int MessageEventId = 1; [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] public void Message(string message) { if (IsEnabled()) { WriteEvent(MessageEventId, message); } } private const int ServiceTypeRegisteredEventId = 3; [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}")] public void ServiceTypeRegistered(int hostProcessId, string serviceType) { WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); } [NonEvent] public void ServiceHostInitializationFailed(Exception e) { ServiceHostInitializationFailed(e.ToString()); } private const int ServiceHostInitializationFailedEventId = 4; [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed: {0}")] private void ServiceHostInitializationFailed(string exception) { WriteEvent(ServiceHostInitializationFailedEventId, exception); } [NonEvent] public void ServiceWebHostBuilderFailed(Exception e) { ServiceWebHostBuilderFailed(e.ToString()); } private const int ServiceWebHostBuilderFailedEventId = 5; [Event(ServiceWebHostBuilderFailedEventId, Level = EventLevel.Error, Message = "Service Owin Web Host Builder Failed: {0}")] private void ServiceWebHostBuilderFailed(string exception) { WriteEvent(ServiceWebHostBuilderFailedEventId, exception); } } ================================================ FILE: samples/ServiceFabric/ApiGateway/WebCommunicationListener.cs ================================================ using Microsoft.ServiceFabric.Services.Communication.Runtime; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Samples.Web; using System.Fabric; using System.Globalization; namespace Ocelot.Samples.ServiceFabric.ApiGateway; public class WebCommunicationListener : ICommunicationListener { private readonly string _appRoot; private readonly ServiceContext _serviceInitializationParameters; private string _listeningAddress; private string _publishAddress; // OWIN server handle. private WebApplication _webApp; public WebCommunicationListener(string appRoot, ServiceContext serviceInitializationParameters) { _appRoot = appRoot; _serviceInitializationParameters = serviceInitializationParameters; } public async Task OpenAsync(CancellationToken cancellationToken) { ServiceEventSource.Current.Message("Initialize"); var serviceEndpoint = _serviceInitializationParameters.CodePackageActivationContext.GetEndpoint("WebEndpoint"); var port = serviceEndpoint.Port; _listeningAddress = string.Format( CultureInfo.InvariantCulture, "http://+:{0}/{1}", port, string.IsNullOrWhiteSpace(_appRoot) ? string.Empty : _appRoot.TrimEnd('/') + '/'); _publishAddress = _listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN); ServiceEventSource.Current.Message("Starting web server on {0}", _listeningAddress); try { _ = OcelotHostBuilder.Create(); var builder = WebApplication.CreateBuilder(); //(args); builder.WebHost.UseUrls(_listeningAddress); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services .AddOcelot(builder.Configuration); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } _webApp = builder.Build(); await _webApp.UseOcelot(); await _webApp.RunAsync(); // .Start(); } catch (Exception ex) { ServiceEventSource.Current.ServiceWebHostBuilderFailed(ex); } return _publishAddress; } public Task CloseAsync(CancellationToken cancellationToken) => StopAll(cancellationToken); public void Abort() => StopAll().GetAwaiter().GetResult(); /// Stops, cancels, and disposes everything. private Task StopAll(CancellationToken cancellationToken = default) { try { if (_webApp != null) { ServiceEventSource.Current.Message("Stopping web server."); return _webApp.StopAsync(cancellationToken); } } catch (ObjectDisposedException) { } return Task.CompletedTask; } } ================================================ FILE: samples/ServiceFabric/ApiGateway/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: samples/ServiceFabric/ApiGateway/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: samples/ServiceFabric/ApiGateway/ocelot.json ================================================ { "Routes": [ { "DownstreamScheme": "http", "DownstreamPathTemplate": "/api/values", "UpstreamPathTemplate": "/EquipmentInterfaces", "UpstreamHttpMethod": [ "Get" ], "ServiceName": "OcelotServiceApplication/OcelotApplicationService" } ], "GlobalConfiguration": { "RequestIdKey": "Oc-RequestId", "ServiceDiscoveryProvider": { "Host": "localhost", "Port": 19081, "Type": "ServiceFabric" } } } ================================================ FILE: samples/ServiceFabric/ApiGateway/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==" }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==" }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==" }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/ServiceFabric/CONTRIBUTING.md ================================================ # Contributing to Azure samples Thank you for your interest in contributing to Azure samples! ## Ways to contribute You can contribute to [Azure samples](https://azure.microsoft.com/documentation/samples/) in a few different ways: - Submit feedback on [this sample page](https://azure.microsoft.com/documentation/samples/service-fabric-dotnet-web-reference-app/) whether it was helpful or not. - Submit issues through [issue tracker](https://github.com/Azure-Samples/service-fabric-dotnet-web-reference-app/issues) on GitHub. We are actively monitoring the issues and improving our samples. - If you wish to make code changes to samples, or contribute something new, please follow the [GitHub Forks / Pull requests model](https://help.github.com/articles/fork-a-repo/): Fork the sample repo, make the change and propose it back by submitting a pull request. ================================================ FILE: samples/ServiceFabric/DownstreamService/ApiGateway.cs ================================================ using Microsoft.ServiceFabric.Services.Communication.AspNetCore; using Microsoft.ServiceFabric.Services.Communication.Runtime; using Microsoft.ServiceFabric.Services.Runtime; using Ocelot.Samples.Web; using System.Fabric; namespace Ocelot.Samples.ServiceFabric.DownstreamService; /// /// The FabricRuntime creates an instance of this class for each service type instance. /// internal sealed class ApiGateway : StatelessService { public ApiGateway(StatelessServiceContext context) : base(context) { } /// /// Optional override to create listeners (like tcp, http) for this service instance. /// /// The collection of listeners. protected override IEnumerable CreateServiceInstanceListeners() { return new ServiceInstanceListener[] { new(serviceContext => new KestrelCommunicationListener(serviceContext, "ServiceEndpoint", (url, listener) => { Console.WriteLine($"Starting Kestrel on {url}"); ServiceEventSource.Current.ServiceMessage(serviceContext, $"Starting Kestrel on {url}"); _ = OcelotHostBuilder.Create(); var builder = WebApplication.CreateBuilder(); //(args); builder.Services .AddSingleton(serviceContext) .AddControllers(); builder.WebHost //.UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseServiceFabricIntegration(listener, ServiceFabricIntegrationOptions.UseUniqueServiceUrl) .UseUrls(url); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); app.MapControllers(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } return app; })) }; } } ================================================ FILE: samples/ServiceFabric/DownstreamService/Controllers/ValuesController.cs ================================================ using Microsoft.AspNetCore.Mvc; namespace Ocelot.Samples.ServiceFabric.DownstreamService.Controllers; [Route("api/[controller]")] public class ValuesController : Controller { // GET api/values [HttpGet] public IEnumerable Get() { return new[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public void Post([FromBody] string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody] string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { } } ================================================ FILE: samples/ServiceFabric/DownstreamService/Ocelot.Samples.ServiceFabric.DownstreamService.csproj ================================================ Stateless Service Application net8.0;net9.0;net10.0 enable enable false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway x64 Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway https://github.com/ThreeMammals/Ocelot/tree/main/samples/ServiceFabric README.md https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/ServiceFabric/DownstreamService/Program.cs ================================================ using Microsoft.ServiceFabric.Services.Runtime; using Ocelot.Samples.ServiceFabric.DownstreamService; try { // The ServiceManifest.XML file defines one or more service type names. // Registering a service maps a service type name to a .NET type. // When Service Fabric creates an instance of this service type, // an instance of the class is created in this host process. await ServiceRuntime.RegisterServiceAsync("OcelotApplicationServiceType", (context) => new ApiGateway(context)); ServiceEventSource.Current.ServiceTypeRegistered(Environment.ProcessId, nameof(ApiGateway)); // Prevents this host process from terminating so services keeps running. Thread.Sleep(Timeout.Infinite); } catch (Exception e) { ServiceEventSource.Current.ServiceHostInitializationFailed(e.ToString()); throw; } ================================================ FILE: samples/ServiceFabric/DownstreamService/Properties/launchSettings.json ================================================ { "profiles": { "https": { "commandName": "Project", "launchBrowser": true, "launchUrl": "api/values", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "https://localhost:7788;http://localhost:5566" } } } ================================================ FILE: samples/ServiceFabric/DownstreamService/ServiceEventSource.cs ================================================ using System.Diagnostics.Tracing; using System.Fabric; namespace Ocelot.Samples.ServiceFabric.DownstreamService; [EventSource(Name = "MyCompany-ServiceOcelotApplication-OcelotService")] internal sealed class ServiceEventSource : EventSource { public static readonly ServiceEventSource Current = new(); static ServiceEventSource() { // A workaround for the problem where ETW activities do not get tracked until Tasks infrastructure is initialized. // This problem will be fixed in .NET Framework 4.6.2. Task.Run(() => { }); } // Instance constructor is private to enforce singleton semantics private ServiceEventSource() : base() { } #region Keywords // Event keywords can be used to categorize events. // Each keyword is a bit flag. A single event can be associated with multiple keywords (via EventAttribute.Keywords property). // Keywords must be defined as a public class named 'Keywords' inside EventSource that uses them. public static class Keywords { public const EventKeywords Requests = (EventKeywords)0x1L; public const EventKeywords ServiceInitialization = (EventKeywords)0x2L; } #endregion #region Events // Define an instance method for each event you want to record and apply an [Event] attribute to it. // The method name is the name of the event. // Pass any parameters you want to record with the event (only primitive integer types, DateTime, Guid & string are allowed). // Each event method implementation should check whether the event source is enabled, and if it is, call WriteEvent() method to raise the event. // The number and types of arguments passed to every event method must exactly match what is passed to WriteEvent(). // Put [NonEvent] attribute on all methods that do not define an event. // For more information see https://msdn.microsoft.com/en-us/library/system.diagnostics.tracing.eventsource.aspx [NonEvent] public void Message(string message, params object[] args) { if (IsEnabled()) { var finalMessage = string.Format(message, args); Message(finalMessage); } } private const int MessageEventId = 1; [Event(MessageEventId, Level = EventLevel.Informational, Message = "{0}")] public void Message(string message) { if (IsEnabled()) { WriteEvent(MessageEventId, message); } } [NonEvent] public void ServiceMessage(ServiceContext serviceContext, string message, params object[] args) { if (IsEnabled()) { var finalMessage = string.Format(message, args); ServiceMessage( serviceContext.ServiceName.ToString(), serviceContext.ServiceTypeName, GetReplicaOrInstanceId(serviceContext), serviceContext.PartitionId, serviceContext.CodePackageActivationContext.ApplicationName, serviceContext.CodePackageActivationContext.ApplicationTypeName, serviceContext.NodeContext.NodeName, finalMessage); } } // For very high-frequency events it might be advantageous to raise events using WriteEventCore API. // This results in more efficient parameter handling, but requires explicit allocation of EventData structure and unsafe code. // To enable this code path, define UNSAFE conditional compilation symbol and turn on unsafe code support in project properties. private const int ServiceMessageEventId = 2; [Event(ServiceMessageEventId, Level = EventLevel.Informational, Message = "{7}")] private #if UNSAFE unsafe #endif void ServiceMessage( string serviceName, string serviceTypeName, long replicaOrInstanceId, Guid partitionId, string applicationName, string applicationTypeName, string nodeName, string message) { #if !UNSAFE WriteEvent(ServiceMessageEventId, serviceName, serviceTypeName, replicaOrInstanceId, partitionId, applicationName, applicationTypeName, nodeName, message); #else const int numArgs = 8; fixed (char* pServiceName = serviceName, pServiceTypeName = serviceTypeName, pApplicationName = applicationName, pApplicationTypeName = applicationTypeName, pNodeName = nodeName, pMessage = message) { EventData* eventData = stackalloc EventData[numArgs]; eventData[0] = new EventData { DataPointer = (IntPtr) pServiceName, Size = SizeInBytes(serviceName) }; eventData[1] = new EventData { DataPointer = (IntPtr) pServiceTypeName, Size = SizeInBytes(serviceTypeName) }; eventData[2] = new EventData { DataPointer = (IntPtr) (&replicaOrInstanceId), Size = sizeof(long) }; eventData[3] = new EventData { DataPointer = (IntPtr) (&partitionId), Size = sizeof(Guid) }; eventData[4] = new EventData { DataPointer = (IntPtr) pApplicationName, Size = SizeInBytes(applicationName) }; eventData[5] = new EventData { DataPointer = (IntPtr) pApplicationTypeName, Size = SizeInBytes(applicationTypeName) }; eventData[6] = new EventData { DataPointer = (IntPtr) pNodeName, Size = SizeInBytes(nodeName) }; eventData[7] = new EventData { DataPointer = (IntPtr) pMessage, Size = SizeInBytes(message) }; WriteEventCore(ServiceMessageEventId, numArgs, eventData); } #endif } private const int ServiceTypeRegisteredEventId = 3; [Event(ServiceTypeRegisteredEventId, Level = EventLevel.Informational, Message = "Service host process {0} registered service type {1}", Keywords = Keywords.ServiceInitialization)] public void ServiceTypeRegistered(int hostProcessId, string serviceType) { WriteEvent(ServiceTypeRegisteredEventId, hostProcessId, serviceType); } private const int ServiceHostInitializationFailedEventId = 4; [Event(ServiceHostInitializationFailedEventId, Level = EventLevel.Error, Message = "Service host initialization failed", Keywords = Keywords.ServiceInitialization)] public void ServiceHostInitializationFailed(string exception) { WriteEvent(ServiceHostInitializationFailedEventId, exception); } // A pair of events sharing the same name prefix with a "Start"/"Stop" suffix implicitly marks boundaries of an event tracing activity. // These activities can be automatically picked up by debugging and profiling tools, which can compute their execution time, child activities, // and other statistics. private const int ServiceRequestStartEventId = 5; [Event(ServiceRequestStartEventId, Level = EventLevel.Informational, Message = "Service request '{0}' started", Keywords = Keywords.Requests)] public void ServiceRequestStart(string requestTypeName) { WriteEvent(ServiceRequestStartEventId, requestTypeName); } private const int ServiceRequestStopEventId = 6; [Event(ServiceRequestStopEventId, Level = EventLevel.Informational, Message = "Service request '{0}' finished", Keywords = Keywords.Requests)] public void ServiceRequestStop(string requestTypeName, string exception = "") { WriteEvent(ServiceRequestStopEventId, requestTypeName, exception); } #endregion #region Private methods private static long GetReplicaOrInstanceId(ServiceContext context) { if (context is StatelessServiceContext stateless) { return stateless.InstanceId; } if (context is StatefulServiceContext stateful) { return stateful.ReplicaId; } throw new NotSupportedException("Context type not supported."); } #if UNSAFE private int SizeInBytes(string s) { if (s == null) { return 0; } else { return (s.Length + 1) * sizeof(char); } } #endif #endregion } ================================================ FILE: samples/ServiceFabric/DownstreamService/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==", "dependencies": { "System.Memory": "4.5.5" } }, "Microsoft.ServiceFabric.AspNetCore.Kestrel": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==", "dependencies": { "Microsoft.ServiceFabric.AspNetCore.Abstractions": "[8.3.475]" } }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "Microsoft.ServiceFabric.AspNetCore.Abstractions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==", "dependencies": { "Microsoft.ServiceFabric.Services": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "System.Memory": { "type": "Transitive", "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "ocelot.samples.web": { "type": "Project" } }, "net8.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==", "dependencies": { "System.Memory": "4.5.5" } }, "Microsoft.ServiceFabric.AspNetCore.Kestrel": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==", "dependencies": { "Microsoft.ServiceFabric.AspNetCore.Abstractions": "[8.3.475]" } }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "Microsoft.ServiceFabric.AspNetCore.Abstractions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==", "dependencies": { "Microsoft.ServiceFabric.Services": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "System.Memory": { "type": "Transitive", "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "ocelot.samples.web": { "type": "Project" } }, "net9.0": { "Microsoft.ServiceFabric": { "type": "Direct", "requested": "[11.3.475, )", "resolved": "11.3.475", "contentHash": "PvGRYI84Zaco8mfJlzIlaTaoPixkWOLTt1D+GzRNJZ4G/vPZYHQaqJE9cpcL0X4MVsnGMXQTKeeF51cglgAD0g==", "dependencies": { "System.Memory": "4.5.5" } }, "Microsoft.ServiceFabric.AspNetCore.Kestrel": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "4owyiOoi3ytZY+U2/Q3dzcxhb8LztIpw0yRSYCXevsTK4cFLUYh+cSrkG/DVFXeLeFNJa2IZ8uQCyLBF9yDkbw==", "dependencies": { "Microsoft.ServiceFabric.AspNetCore.Abstractions": "[8.3.475]" } }, "Microsoft.ServiceFabric.Services": { "type": "Direct", "requested": "[8.3.475, )", "resolved": "8.3.475", "contentHash": "7jB7KPqTGa6qQM7YyQWA5X/cjkEGWSyYMvIsf32ceCSoG06txDE657n/PgePnbZXRlCePnhfM948l9+aXNwuNQ==", "dependencies": { "Microsoft.ServiceFabric.Data": "[8.3.475]", "Microsoft.ServiceFabric.Diagnostics.Internal": "[8.3.475]" } }, "Microsoft.ServiceFabric.AspNetCore.Abstractions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "wut67yR0P9R3JfeJjaie1uUfb7wjjbcb/851e7c7gnR7pBomSN3YDmOWAoYlseOHsxkSj0b3yGc6o19VTM1iaQ==", "dependencies": { "Microsoft.ServiceFabric.Services": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "FderlvcKGLSN7p3RSv51g51x9RIlQsqyY0tN56rNiVzDOccJ/4oUkvnf/kA98CY+xoaNh1hljVrVyt7wvKijYg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]", "Microsoft.ServiceFabric.Data.Extensions": "[8.3.475]", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Extensions": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "kGiyL9uJrVquI1jjbOmY9J+fqLBLqJZk2PhWGCOv9dea3/2j9/oimpLwoi9I6nOflSTZSVfmrCy4SR4wpe2EcA==", "dependencies": { "Microsoft.ServiceFabric": "11.3.475", "Microsoft.ServiceFabric.Data.Interfaces": "[8.3.475]" } }, "Microsoft.ServiceFabric.Data.Interfaces": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "7lw+YyR3jPowcfqrsgGpbxVwlXphs3ZrkTmnqjJH+3zI0bx/+6p5krpOeQCmlShIkzH06KbIK/7sgO23w1Vjmg==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "Microsoft.ServiceFabric.Diagnostics.Internal": { "type": "Transitive", "resolved": "8.3.475", "contentHash": "yITbdO0O9D51uqLIjw5BTHJHrpRXK44aqjF/+w6WUBVMKAYfcraVA+W8fmYsB/bHrFEDSIAl6fEbDF3AOcoNQA==", "dependencies": { "Microsoft.ServiceFabric": "[11.3.475]" } }, "System.Memory": { "type": "Transitive", "resolved": "4.5.5", "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "ocelot.samples.web": { "type": "Project" } } } } ================================================ FILE: samples/ServiceFabric/LICENSE.md ================================================ MIT License Copyright (c) 2016 Azure Samples Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: samples/ServiceFabric/Ocelot.Samples.ServiceFabric.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.13.35919.96 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.ServiceFabric.ApiGateway", "ApiGateway\Ocelot.Samples.ServiceFabric.ApiGateway.csproj", "{D951C29E-FBB5-175A-83B8-41477A3B45C6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.ServiceFabric.DownstreamService", "DownstreamService\Ocelot.Samples.ServiceFabric.DownstreamService.csproj", "{ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Web", "..\Web\Ocelot.Samples.Web.csproj", "{72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot", "..\..\src\Ocelot\Ocelot.csproj", "{C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {D951C29E-FBB5-175A-83B8-41477A3B45C6}.Debug|Any CPU.ActiveCfg = Debug|x64 {D951C29E-FBB5-175A-83B8-41477A3B45C6}.Debug|Any CPU.Build.0 = Debug|x64 {D951C29E-FBB5-175A-83B8-41477A3B45C6}.Release|Any CPU.ActiveCfg = Release|Any CPU {D951C29E-FBB5-175A-83B8-41477A3B45C6}.Release|Any CPU.Build.0 = Release|Any CPU {ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACA3A2E7-76EC-2C6A-EA8D-62F80CED67D2}.Release|Any CPU.Build.0 = Release|Any CPU {72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {72C2C3AA-51E7-17B3-35A4-F7CE9F5B2AE8}.Release|Any CPU.Build.0 = Release|Any CPU {C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C4EB61DE-2D16-D4F9-31D3-1F791DB3852A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {17D35415-616B-4BAA-A82F-37F7BAC0FC77} EndGlobalSection EndGlobal ================================================ FILE: samples/ServiceFabric/OcelotApplication/ApplicationManifest.xml ================================================ ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.cmd ================================================ dotnet %~dp0\OcelotApplicationApiGateway.dll exit /b %errorlevel% ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Code/entryPoint.sh ================================================ #!/usr/bin/env bash check_errs() { # Function. Parameter 1 is the return code if [ "${1}" -ne "0" ]; then # make our script exit with the right error code. exit ${1} fi } DIR=`dirname $0` echo 0x3f > /proc/self/coredump_filter source $DIR/dotnet-include.sh dotnet $DIR/OcelotApplicationApiGateway.dll $@ check_errs $? ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/Settings.xml ================================================ ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Config/_readme.txt ================================================ contains a Settings.xml file, that can specify parameters for the service Configuration packages describe user-defined, application-overridable configuration settings (sections of key-value pairs) required for running service replicas/instances of service types specified in the ser-vice manifest. The configuration settings must be stored in Settings.xml in the config package folder. The service developer uses Service Fabric APIs to locate the package folders and read applica-tion-overridable configuration settings. The service developer can also register callbacks that are in-voked when any of the configuration packages specified in the service manifest are upgraded and re-reads new configuration settings inside that callback. This means that Service Fabric will not recycle EXEs and DLLHOSTs specified in the host and support packages when configuration packages are up-graded. ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/Data/_readme.txt ================================================ Data packages contain data files like custom dictionaries, non-overridable configuration files, custom initialized data files, etc. Service Fabric will recycle all EXEs and DLLHOSTs specified in the host and support packages when any of the data packages specified inside service manifest are upgraded. ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Linux.xml ================================================ entryPoint.sh ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest-Windows.xml ================================================ entryPoint.cmd ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationApiGatewayPkg/ServiceManifest.xml ================================================ entryPoint.cmd ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.cmd ================================================ dotnet %~dp0\OcelotApplicationService.dll exit /b %errorlevel% ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Code/entryPoint.sh ================================================ #!/usr/bin/env bash check_errs() { # Function. Parameter 1 is the return code if [ "${1}" -ne "0" ]; then # make our script exit with the right error code. exit ${1} fi } DIR=`dirname $0` source $DIR/dotnet-include.sh dotnet $DIR/OcelotApplicationService.dll $@ check_errs $? ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/Settings.xml ================================================ ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Config/_readme.txt ================================================ contains a Settings.xml file, that can specify parameters for the service Configuration packages describe user-defined, application-overridable configuration settings (sections of key-value pairs) required for running service replicas/instances of service types specified in the ser-vice manifest. The configuration settings must be stored in Settings.xml in the config package folder. The service developer uses Service Fabric APIs to locate the package folders and read applica-tion-overridable configuration settings. The service developer can also register callbacks that are in-voked when any of the configuration packages specified in the service manifest are upgraded and re-reads new configuration settings inside that callback. This means that Service Fabric will not recycle EXEs and DLLHOSTs specified in the host and support packages when configuration packages are up-graded. ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/Data/_readme.txt ================================================ Data packages contain data files like custom dictionaries, non-overridable configuration files, custom initialized data files, etc. Service Fabric will recycle all EXEs and DLLHOSTs specified in the host and support packages when any of the data packages specified inside service manifest are upgraded. ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Linux.xml ================================================ entryPoint.sh ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest-Windows.xml ================================================ entryPoint.cmd ================================================ FILE: samples/ServiceFabric/OcelotApplication/OcelotApplicationServicePkg/ServiceManifest.xml ================================================ entryPoint.cmd ================================================ FILE: samples/ServiceFabric/README.md ================================================ --- services: service-fabric platforms: dotnet author: raunakpandya edited by Tom Pallister for Ocelot --- # Ocelot Service Fabric example This shows a service fabric cluster with Ocelot exposed over HTTP accessing services in the cluster via the naming service. If you want to try and use Ocelot with Service Fabric I reccomend using this as a starting point. If you want to use statefull / actors you must send the PartitionKind and PartitionKey to Ocelot as query string parameters. I have not tested this sample on Service Fabric hosted on Linux just a Windows dev cluster. This sample assumes a good understanding of Service Fabric. The rest of this document is from the Microsoft asp.net core service fabric getting started guide. # Getting started with Service Fabric with .NET Core This repository contains a set of simple sample projects to help you getting started with Service Fabric on Linux with .NET Core as the framework. As a pre requisite ensure you have the Service Fabric C# SDK installed on ubuntu box. Follow these instruction to [prepare your development environment on Linux][service-fabric-Linux-getting-started] ### Folder Hierarchy * src/ - Source of the application divided by different modules by sub-folders. * <application package folder>/ - Service Fabric Application folder heirarchy. After compilation the executables are placed in code subfolders. * build.sh - Script to build source on Linux shell. * build.ps1 - PowerShell script to build source on Windows. * install.sh - Script to install Application from Linux shell. * install.ps1 - PowerShell script to install application from Windows. Before calling this script run Connect-ServiceFabricCluster localhost:19000 or however you prefer to connect. * uninstall.sh - Script to uninstall application from Linux shell. * uninstall.ps1 - PowerShell script to unintall application from Windows. * dotnet-include.sh - Script to conditionally handle RHEL dotnet cli through scl(software collections) # Testing Once everything is up and running on your dev cluster visit http://localhost:31002/EquipmentInterfaces and you should see the following returned. ```json ["value1","value2"] ``` If you get any errors please check the service fabric logs and let me know if you need help. ## More information The [Service Fabric documentation][service-fabric-docs] includes a rich set of tutorials and conceptual articles, which serve as a good complement to the samples. [service-fabric-programming-models]: https://azure.microsoft.com/en-us/documentation/articles/service-fabric-choose-framework/ [service-fabric-docs]: http://aka.ms/servicefabricdocs [service-fabric-Linux-getting-started]: https://azure.microsoft.com/en-us/documentation/articles/service-fabric-get-started-linux/ ================================================ FILE: samples/ServiceFabric/build.bat ================================================ cd ./src/OcelotApplicationService/ dotnet restore -s https://api.nuget.org/v3/index.json dotnet build dotnet publish -o ../../OcelotApplication/OcelotApplicationServicePkg/Code cd ../../ cd ./src/OcelotApplicationApiGateway/ dotnet restore -s https://api.nuget.org/v3/index.json dotnet build dotnet publish -o ../../OcelotApplication/OcelotApplicationApiGatewayPkg/Code cd ../../ ================================================ FILE: samples/ServiceFabric/build.sh ================================================ #!/bin/bash DIR=`dirname $0` source $DIR/dotnet-include.sh cd $DIR/src/OcelotApplicationService/ dotnet restore -s https://api.nuget.org/v3/index.json dotnet build dotnet publish -o ../../OcelotApplication/OcelotApplicationServicePkg/Code cd - cd $DIR/src/OcelotApplicationApiGateway/ dotnet restore -s https://api.nuget.org/v3/index.json dotnet build dotnet publish -o ../../OcelotApplication/OcelotApplicationApiGatewayPkg/Code cd - ================================================ FILE: samples/ServiceFabric/dotnet-include.sh ================================================ #!/bin/bash . /etc/os-release linuxDistrib=$ID if [ $linuxDistrib = "rhel" ]; then source scl_source enable rh-dotnet20 exitCode=$? if [ $exitCode != 0 ]; then echo "Failed: source scl_source enable rh-dotnet20 : ExitCode: $exitCode" exit $exitCode fi fi ================================================ FILE: samples/ServiceFabric/install.ps1 ================================================ $AppPath = "$PSScriptRoot\OcelotApplication" $sdkInstallPath = (Get-ItemProperty 'HKLM:\Software\Microsoft\Service Fabric SDK').FabricSDKInstallPath $sfSdkPsModulePath = $sdkInstallPath + "Tools\PSModule\ServiceFabricSDK" Import-Module $sfSdkPsModulePath\ServiceFabricSDK.psm1 $StatefulServiceManifestlocation = $AppPath + "\OcelotApplicationServicePkg\" $StatefulServiceManifestlocationLinux = $StatefulServiceManifestlocation + "\ServiceManifest-Linux.xml" $StatefulServiceManifestlocationWindows = $StatefulServiceManifestlocation + "\ServiceManifest-Windows.xml" $StatefulServiceManifestlocationFinal= $StatefulServiceManifestlocation + "ServiceManifest.xml" Copy-Item -Path $StatefulServiceManifestlocationWindows -Destination $StatefulServiceManifestlocationFinal -Force $WebServiceManifestlocation = $AppPath + "\OcelotApplicationApiGatewayPkg\" $WebServiceManifestlocationLinux = $WebServiceManifestlocation + "\ServiceManifest-Linux.xml" $WebServiceManifestlocationWindows = $WebServiceManifestlocation + "\ServiceManifest-Windows.xml" $WebServiceManifestlocationFinal= $WebServiceManifestlocation + "ServiceManifest.xml" Copy-Item -Path $WebServiceManifestlocationWindows -Destination $WebServiceManifestlocationFinal -Force Copy-ServiceFabricApplicationPackage -ApplicationPackagePath $AppPath -ApplicationPackagePathInImageStore OcelotServiceApplicationType -ImageStoreConnectionString (Get-ImageStoreConnectionStringFromClusterManifest(Get-ServiceFabricClusterManifest)) -TimeoutSec 1800 Register-ServiceFabricApplicationType OcelotServiceApplicationType New-ServiceFabricApplication fabric:/OcelotServiceApplication OcelotServiceApplicationType 1.0.0 ================================================ FILE: samples/ServiceFabric/install.sh ================================================ #!/bin/bash DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" appPkg="$DIR/OcelotServiceApplication" WebServiceManifestlocation="$appPkg/OcelotApplicationApiGatewayPkg" WebServiceManifestlocationLinux="$WebServiceManifestlocation/ServiceManifest-Linux.xml" WebServiceManifestlocationWindows="$WebServiceManifestlocation/ServiceManifest-Windows.xml" WebServiceManifestlocation="$WebServiceManifestlocation/ServiceManifest.xml" cp $WebServiceManifestlocationLinux $WebServiceManifestlocation StatefulServiceManifestlocation="$appPkg/OcelotApplicationServicePkg" StatefulServiceManifestlocationLinux="$StatefulServiceManifestlocation/ServiceManifest-Linux.xml" StatefulServiceManifestlocationWindows="$StatefulServiceManifestlocation/ServiceManifest-Windows.xml" StatefulServiceManifestlocation="$StatefulServiceManifestlocation/ServiceManifest.xml" cp $StatefulServiceManifestlocationLinux $StatefulServiceManifestlocation cp dotnet-include.sh ./OcelotServiceApplication/OcelotApplicationServicePkg/Code cp dotnet-include.sh ./OcelotServiceApplication/OcelotApplicationApiGatewayPkg/Code sfctl application upload --path OcelotServiceApplication --show-progress sfctl application provision --application-type-build-path OcelotServiceApplication sfctl application create --app-name fabric:/OcelotServiceApplication --app-type OcelotServiceApplicationType --app-version 1.0.0 ================================================ FILE: samples/ServiceFabric/uninstall.ps1 ================================================ Remove-ServiceFabricApplication fabric:/OcelotServiceApplication Unregister-ServiceFabricApplicationType OcelotServiceApplicationType 1.0.0 ================================================ FILE: samples/ServiceFabric/uninstall.sh ================================================ #!/bin/bash -x sfctl application delete --application-id OcelotServiceApplication sfctl application unprovision --application-type-name OcelotServiceApplicationType --application-type-version 1.0.0 sfctl store delete --content-path OcelotServiceApplication ================================================ FILE: samples/Web/DownstreamHostBuilder.cs ================================================ using Microsoft.AspNetCore; namespace Ocelot.Samples.Web; public sealed class DownstreamHostBuilder : WebHostBuilder { public static IWebHostBuilder Create() => WebHost .CreateDefaultBuilder() .UseDefaultServiceProvider(WithEnabledValidateScopes); public static IWebHostBuilder Create(Action configure) => WebHost .CreateDefaultBuilder() .UseDefaultServiceProvider(configure + WithEnabledValidateScopes); public static IWebHostBuilder Create(string[] args) => WebHost .CreateDefaultBuilder(args) .UseDefaultServiceProvider(WithEnabledValidateScopes); public static IWebHostBuilder Create(string[] args, Action configure) => WebHost .CreateDefaultBuilder(args) .UseDefaultServiceProvider(configure + WithEnabledValidateScopes); public static void WithEnabledValidateScopes(ServiceProviderOptions options) => options.ValidateScopes = true; // TODO Add more standard Ocelot setup public static IWebHostBuilder BasicSetup() => Create(); // in CreateDefaultBuilder() implicitly calls -> .UseKestrel().UseContentRoot(Directory.GetCurrentDirectory()); } ================================================ FILE: samples/Web/Ocelot.Samples.Web.csproj ================================================  net8.0;net9.0;net10.0 enable enable Library false 0.0.0-dev 24.1.0 © 2025 Three Mammals. MIT licensed OSS Ocelot Gateway Raman Maksimchuk Three Mammals Ocelot Gateway Shared library for all Ocelot samples https://github.com/ThreeMammals/Ocelot/tree/main/samples/Web https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: samples/Web/OcelotHostBuilder.cs ================================================ using Microsoft.AspNetCore; namespace Ocelot.Samples.Web; public sealed class OcelotHostBuilder : WebHostBuilder { public static IWebHostBuilder Create() => WebHost .CreateDefaultBuilder() .UseDefaultServiceProvider(WithEnabledValidateScopes); public static IWebHostBuilder Create(Action configure) => WebHost .CreateDefaultBuilder() .UseDefaultServiceProvider(configure + WithEnabledValidateScopes); public static IWebHostBuilder Create(string[] args) => WebHost .CreateDefaultBuilder(args) .UseDefaultServiceProvider(WithEnabledValidateScopes); public static IWebHostBuilder Create(string[] args, Action configure) => WebHost .CreateDefaultBuilder(args) .UseDefaultServiceProvider(configure + WithEnabledValidateScopes); public static void WithEnabledValidateScopes(ServiceProviderOptions options) => options.ValidateScopes = true; // TODO Add more standard Ocelot setup public static IWebHostBuilder BasicSetup() => Create(); // in CreateDefaultBuilder() implicitly calls -> .UseKestrel().UseContentRoot(Directory.GetCurrentDirectory()); } ================================================ FILE: samples/Web/Properties/launchSettings.json ================================================ { "profiles": { "Ocelot.Samples.Web": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:60184;http://localhost:60185" } } } ================================================ FILE: samples/Web/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": {}, "net8.0": {}, "net9.0": {} } } ================================================ FILE: src/Ocelot/Administration/AdministrationPath.cs ================================================ namespace Ocelot.Administration; public class AdministrationPath : IAdministrationPath { public AdministrationPath(string path, string apiSecret, Uri externalJwtServerUrl = null) { Path = path; IssuerSigningKey = apiSecret; ExternalJwtSigningUrl = externalJwtServerUrl; } public string Path { get; } public string IssuerSigningKey { get; } public Uri ExternalJwtSigningUrl { get; } } ================================================ FILE: src/Ocelot/Administration/FileConfigurationController.cs ================================================ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; namespace Ocelot.Administration; // [ApiController] // TODO: Make it ApiController [Authorize] [Route("configuration")] public class FileConfigurationController : Controller { private readonly IFileConfigurationRepository _repo; private readonly IFileConfigurationSetter _setter; public FileConfigurationController(IFileConfigurationRepository repo, IFileConfigurationSetter setter) { _repo = repo; _setter = setter; } [HttpGet] public async Task Get() { var response = await _repo.Get(); if (response.IsError) { return new BadRequestObjectResult(response.Errors); } return new OkObjectResult(response.Data); } [HttpPost] public async Task Post([FromBody] FileConfiguration fileConfiguration) { try { var response = await _setter.Set(fileConfiguration); if (response.IsError) { return new BadRequestObjectResult(response.Errors); } return new OkObjectResult(fileConfiguration); } catch (Exception e) { return new BadRequestObjectResult($"{e.Message}:{Environment.NewLine}{e.StackTrace}"); } } } ================================================ FILE: src/Ocelot/Administration/IAdministrationPath.cs ================================================ namespace Ocelot.Administration; public interface IAdministrationPath { string Path { get; } string IssuerSigningKey { get; } Uri ExternalJwtSigningUrl { get; } } ================================================ FILE: src/Ocelot/Administration/OutputCacheController.cs ================================================ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Ocelot.Cache; namespace Ocelot.Administration; // [ApiController] // TODO: Make it ApiController //[Authorize(Policy = "OcelotAdministration")] [Authorize] [Route("outputcache")] public class OutputCacheController : Controller { private readonly IOcelotCache _cache; public OutputCacheController(IOcelotCache cache) { _cache = cache; } [HttpDelete] [Route("{region}")] public IActionResult Delete(string region) { _cache.ClearRegion(region); return new NoContentResult(); } } ================================================ FILE: src/Ocelot/Authentication/AuthenticationMiddleware.cs ================================================ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Authentication; public sealed class AuthenticationMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; public AuthenticationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { _next = next; } public async Task Invoke(HttpContext context) { var request = context.Request; var path = context.Request.Path; var route = context.Items.DownstreamRoute(); if (request.IsOptionsMethod() || !route.IsAuthenticated) { Logger.LogInformation(() => $"No authentication is required for the path '{path}' in the route {route.Name()}."); await _next(context); return; } Logger.LogInformation(() => $"The path '{path}' is an authenticated route! {MiddlewareName} checking if client is authenticated..."); var result = await AuthenticateAsync(context, route); if (result.Principal?.Identity == null) { SetUnauthenticatedError(context, path, null); return; } context.User = result.Principal; if (context.User.Identity.IsAuthenticated) { Logger.LogInformation(() => $"Client has been authenticated for path '{path}' by '{context.User.Identity.AuthenticationType}' scheme."); await _next.Invoke(context); return; } SetUnauthenticatedError(context, path, context.User.Identity.Name); } private void SetUnauthenticatedError(HttpContext httpContext, string path, string userName) { var reason = string.IsNullOrEmpty(userName) ? "was unauthenticated!" : $"by '{userName}' was unauthenticated!"; var error = new UnauthenticatedError($"Request for authenticated route '{path}' {reason}"); Logger.LogWarning(() => $"Client has NOT been authenticated for path '{path}' and pipeline error set. {error};"); httpContext.Items.SetError(error); } private async Task AuthenticateAsync(HttpContext context, DownstreamRoute route) { var notEmptySchemes = route.AuthenticationOptions.AuthenticationProviderKeys .Where(s => !string.IsNullOrWhiteSpace(s)); AuthenticateResult result = null; foreach (var scheme in notEmptySchemes) { try { result = await context.AuthenticateAsync(scheme); if (result?.Succeeded == true) { return result; } } catch (Exception e) { Logger.LogWarning(() => $"Unable to authenticate the client for route '{route.Name()}' using the {scheme} authentication scheme due to error: {e.Message}"); } } return result ?? AuthenticateResult.NoResult(); } } ================================================ FILE: src/Ocelot/Authorization/AuthorizationMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Authorization; public class AuthorizationMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IClaimsAuthorizer _claimsAuthorizer; private readonly IScopesAuthorizer _scopesAuthorizer; public AuthorizationMiddleware(RequestDelegate next, IClaimsAuthorizer claimsAuthorizer, IScopesAuthorizer scopesAuthorizer, IOcelotLoggerFactory loggerFactory) : base(loggerFactory.CreateLogger()) { _next = next; _claimsAuthorizer = claimsAuthorizer; _scopesAuthorizer = scopesAuthorizer; } public async Task Invoke(HttpContext context) { var route = context.Items.DownstreamRoute(); if (!context.IsOptionsMethod() && route.IsAuthenticated) { var authorized = _scopesAuthorizer.Authorize(context.User, route.AuthenticationOptions.AllowedScopes); if (authorized.IsError) { #if DEBUG Logger.LogWarning(() => $"The '{route.Name()}' route encountered authorization errors due to user scopes:{authorized.Errors.ToErrorString(true)}"); #endif context.Items.UpsertErrors(authorized.Errors); return; } if (!authorized.Data) // TODO: Looks like this is never called due to the current ScopesAuthorizer design :D Definitely a good reason to refactor { var error = new UnauthorizedError($"{context.User.Identity.Name} unable to access route {route.Name()}"); #if DEBUG Logger.LogInformation(error.ToString); #endif context.Items.SetError(error); } } if (!context.IsOptionsMethod() && route.IsAuthorized) { var authorized = _claimsAuthorizer.Authorize(context.User, route.RouteClaimsRequirement, context.Items.TemplatePlaceholderNameAndValues()); if (authorized.IsError) { #if DEBUG Logger.LogWarning(() => $"Error whilst authorizing {context.User.Identity.Name} in route {route.Name()}:{authorized.Errors.ToErrorString(true)}"); #endif context.Items.UpsertErrors(authorized.Errors); return; } if (authorized.Data) { #if DEBUG Logger.LogInformation(() => $"{context.User.Identity.Name} has successfully been authorized for {route.Name()}."); #endif await _next.Invoke(context); } else { var error = new UnauthorizedError($"{context.User.Identity.Name} is not authorized to access '{route.Name()}' route. Setting pipeline error."); #if DEBUG Logger.LogInformation(error.ToString); #endif context.Items.SetError(error); } } else { #if DEBUG Logger.LogDebug(() => $"No authorization needed for the route: {route.Name()}"); #endif await _next.Invoke(context); } } } ================================================ FILE: src/Ocelot/Authorization/ClaimValueNotAuthorizedError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Authorization; public class ClaimValueNotAuthorizedError : Error { public ClaimValueNotAuthorizedError(string message) : base(message, OcelotErrorCode.ClaimValueNotAuthorizedError, 403) { } } ================================================ FILE: src/Ocelot/Authorization/ClaimsAuthorizer.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Authorization; /// /// Default authorizer by claims. /// public partial class ClaimsAuthorizer : IClaimsAuthorizer { private readonly IClaimsParser _claimsParser; public ClaimsAuthorizer(IClaimsParser claimsParser) { _claimsParser = claimsParser; } [GeneratedRegex(@"^{(?.+)}$", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex RegexAuthorize(); public Response Authorize( ClaimsPrincipal claimsPrincipal, Dictionary routeClaimsRequirement, List urlPathPlaceholderNameAndValues ) { foreach (var required in routeClaimsRequirement) { var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, required.Key); if (values.IsError) { return new ErrorResponse(values.Errors); } if (values.Data != null) { // dynamic claim var match = RegexAuthorize().Match(required.Value); if (match.Success) { var variableName = match.Captures[0].Value; var matchingPlaceholders = urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Take(2).ToArray(); if (matchingPlaceholders.Length == 1) { // match var actualValue = matchingPlaceholders[0].Value; var authorized = values.Data.Contains(actualValue); if (!authorized) { return new ErrorResponse(new ClaimValueNotAuthorizedError( $"dynamic claim value for {variableName} of {string.Join(", ", values.Data)} is not the same as required value: {actualValue}")); } } else { // config error if (matchingPlaceholders.Length == 0) { return new ErrorResponse(new ClaimValueNotAuthorizedError( $"config error: requires variable claim value: {variableName} placeholders does not contain that variable: {string.Join(", ", urlPathPlaceholderNameAndValues.Select(p => p.Name))}")); } else { return new ErrorResponse(new ClaimValueNotAuthorizedError( $"config error: requires variable claim value: {required.Value} but placeholders are ambiguous: {string.Join(", ", urlPathPlaceholderNameAndValues.Where(p => p.Name.Equals(variableName)).Select(p => p.Value))}")); } } } else { // static claim var authorized = values.Data.Contains(required.Value); if (!authorized) { return new ErrorResponse(new ClaimValueNotAuthorizedError( $"claim value: {string.Join(", ", values.Data)} is not the same as required value: {required.Value} for type: {required.Key}")); } } } else { return new ErrorResponse(new UserDoesNotHaveClaimError($"user does not have claim {required.Key}")); } } return new OkResponse(true); } } ================================================ FILE: src/Ocelot/Authorization/IClaimsAuthorizer.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Authorization; public interface IClaimsAuthorizer { Response Authorize( ClaimsPrincipal claimsPrincipal, Dictionary routeClaimsRequirement, List urlPathPlaceholderNameAndValues ); } ================================================ FILE: src/Ocelot/Authorization/IScopesAuthorizer.cs ================================================ using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Authorization; public interface IScopesAuthorizer { /// /// Checks that the and its collection /// contain at least one value present in the list. /// /// /// Supports the RFC 8693 standard, allowing scope claim values as whitespace-separated strings.
/// RFC 8693 Docs: OAuth 2.0 Token Exchange | 4.2. "scope" (Scopes) Claim. ///
/// If not authorized. /// Claims object from the current authentication provider's token. /// List of allowed scopes for the route. /// if any token scope claim value is in the allowed scopes; otherwise, . Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes); /// Gets the claim type for scope. /// A representing the scope. string ScopeClaim { get; } } ================================================ FILE: src/Ocelot/Authorization/ScopeNotAuthorizedError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.Authorization; public class ScopeNotAuthorizedError : Error { public ScopeNotAuthorizedError(string message) : base(message, OcelotErrorCode.ScopeNotAuthorizedError, StatusCodes.Status403Forbidden) { } } ================================================ FILE: src/Ocelot/Authorization/ScopesAuthorizer.cs ================================================ using Ocelot.Infrastructure.Claims; using Ocelot.Infrastructure.Extensions; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Authorization; public class ScopesAuthorizer : IScopesAuthorizer { public const string Scope = "scope"; public const char SpaceChar = (char)32; private readonly IClaimsParser _claimsParser; public ScopesAuthorizer(IClaimsParser claimsParser) { _claimsParser = claimsParser; } /// public string ScopeClaim => Scope; /// public Response Authorize(ClaimsPrincipal claimsPrincipal, List routeAllowedScopes) { if (routeAllowedScopes == null || routeAllowedScopes.Count == 0) { return new OkResponse(true); } var values = _claimsParser.GetValuesByClaimType(claimsPrincipal.Claims, ScopeClaim); if (values.IsError) { return new ErrorResponse(values.Errors); } IList userScopes = values.Data; // There should not be more than one scope claim that has space-separated value by design // Some providers use array value some space-separated value but not both // https://datatracker.ietf.org/doc/html/rfc8693#name-scope-scopes-claim if (userScopes.Count == 1 && userScopes[0].Contains(SpaceChar)) { userScopes = userScopes[0].Split(SpaceChar, StringSplitOptions.RemoveEmptyEntries); } var matchesScopes = routeAllowedScopes.Intersect(userScopes); if (!matchesScopes.Any()) { return new ErrorResponse( new ScopeNotAuthorizedError($"no one user scope: '{userScopes.Csv()}' match with some allowed scope: '{routeAllowedScopes.Csv()}'")); } return new OkResponse(true); } } ================================================ FILE: src/Ocelot/Authorization/UnauthorizedError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.Authorization; public class UnauthorizedError : Error { public UnauthorizedError(string message) : base(message, OcelotErrorCode.UnauthorizedError, StatusCodes.Status403Forbidden) { } } ================================================ FILE: src/Ocelot/Authorization/UserDoesNotHaveClaimError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.Authorization; public class UserDoesNotHaveClaimError : Error { public UserDoesNotHaveClaimError(string message) : base(message, OcelotErrorCode.UserDoesNotHaveClaimError, StatusCodes.Status403Forbidden) { } } ================================================ FILE: src/Ocelot/Cache/CachedResponse.cs ================================================ namespace Ocelot.Cache; public class CachedResponse { public CachedResponse( HttpStatusCode statusCode, Dictionary> headers, string body, Dictionary> contentHeaders, string reasonPhrase) { StatusCode = statusCode; Headers = headers ?? new(); ContentHeaders = contentHeaders ?? new(); Body = body ?? string.Empty; ReasonPhrase = reasonPhrase; } public HttpStatusCode StatusCode { get; } public Dictionary> Headers { get; } public Dictionary> ContentHeaders { get; } public string Body { get; } public string ReasonPhrase { get; } } ================================================ FILE: src/Ocelot/Cache/DefaultCacheKeyGenerator.cs ================================================ using Ocelot.Configuration; using Ocelot.Request.Middleware; namespace Ocelot.Cache; public class DefaultCacheKeyGenerator : ICacheKeyGenerator { public const char Delimiter = '-'; public async ValueTask GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute) { var builder = new StringBuilder() .Append(downstreamRequest.Method) .Append(Delimiter) .Append(downstreamRequest.OriginalString); var options = downstreamRoute.CacheOptions ?? new(); if (!string.IsNullOrEmpty(options.Header)) { var header = downstreamRequest.Headers.TryGetValues(options.Header, out var values) ? string.Join(string.Empty, values) : string.Empty; builder.Append(Delimiter).Append(header); } if (!options.EnableContentHashing || !downstreamRequest.HasContent) { return MD5Helper.GenerateMd5(builder.ToString()); } var requestContent = await downstreamRequest.Request.Content.ReadAsStringAsync(); builder.Append(Delimiter).Append(requestContent); return MD5Helper.GenerateMd5(builder.ToString()); } } ================================================ FILE: src/Ocelot/Cache/DefaultMemoryCache.cs ================================================ using Microsoft.Extensions.Caching.Memory; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Cache; public class DefaultMemoryCache : IOcelotCache { private readonly IMemoryCache _memoryCache; private readonly ConcurrentDictionary> _regions; public DefaultMemoryCache(IMemoryCache memoryCache) { _memoryCache = memoryCache; _regions = new(); } public bool Add(string key, T value, string region, TimeSpan ttl) { if (ttl.TotalMilliseconds <= 0) { return false; } _memoryCache.Set(key, value, ttl); SetRegion(region, key); return true; } public T AddOrUpdate(string key, T value, string region, TimeSpan ttl) { if (_memoryCache.TryGetValue(key, out _)) { _memoryCache.Remove(key); } Add(key, value, region, ttl); return value; } public T Get(string key, string region) { if (TryGetValue(key, region, out T value)) { return value; } return default; } public void ClearRegion(string region) { if (region.IsNotEmpty() && _regions.TryGetValue(region, out var keys)) { foreach (var key in keys) { _memoryCache.Remove(key); } keys.Clear(); } } private void SetRegion(string region, string key) { if (region.IsNotEmpty() && _regions.TryGetValue(region, out var current)) { if (key.IsNotEmpty() && !current.Contains(key)) { current.Add(key); } } else if (region.IsNotEmpty()) { _regions.TryAdd(region, [key]); } } public bool TryGetValue(string key, string region, out T value) { return _memoryCache.TryGetValue(key, out value); } } ================================================ FILE: src/Ocelot/Cache/ICacheKeyGenerator.cs ================================================ using Ocelot.Configuration; using Ocelot.Request.Middleware; namespace Ocelot.Cache; public interface ICacheKeyGenerator { ValueTask GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute); } ================================================ FILE: src/Ocelot/Cache/IOcelotCache.cs ================================================ namespace Ocelot.Cache; public interface IOcelotCache { /// /// Adds the specified to the cache. /// Use this overload to overrule the configured expiration settings of the cache and to define a custom expiration for this only. /// The Add method will not be successful if the specified already exists within the cache. /// /// The caching key. /// The CacheItem to be added to the cache. /// The region. /// The timeout of absolute expiration. /// if the key was not already added to the cache, otherwise. /// If the or the is . bool Add(string key, T value, string region, TimeSpan ttl); T AddOrUpdate(string key, T value, string region, TimeSpan ttl); T Get(string key, string region); void ClearRegion(string region); bool TryGetValue(string key, string region, out T value); } ================================================ FILE: src/Ocelot/Cache/MD5Helper.cs ================================================ using System.Security.Cryptography; namespace Ocelot.Cache; // TODO: Consider moving to Infrastructure public static class MD5Helper { public static string GenerateMd5(byte[] contentBytes) { var md5 = MD5.Create(); var hash = md5.ComputeHash(contentBytes); var sb = new StringBuilder(); for (var i = 0; i < hash.Length; i++) { sb.Append(hash[i].ToString("X2")); } return sb.ToString(); } public static string GenerateMd5(string contentString) { var contentBytes = Encoding.Unicode.GetBytes(contentString); return GenerateMd5(contentBytes); } } ================================================ FILE: src/Ocelot/Cache/OutputCacheMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Cache; public class OutputCacheMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IOcelotCache _outputCache; private readonly ICacheKeyGenerator _cacheGenerator; public OutputCacheMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IOcelotCache outputCache, ICacheKeyGenerator cacheGenerator) : base(loggerFactory.CreateLogger()) { _next = next; _outputCache = outputCache; _cacheGenerator = cacheGenerator; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); var options = downstreamRoute.CacheOptions; if (!options.UseCache) { await _next.Invoke(httpContext); return; } var downstreamRequest = httpContext.Items.DownstreamRequest(); var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"; var downStreamRequestCacheKey = await _cacheGenerator.GenerateRequestCacheKey(downstreamRequest, downstreamRoute); Logger.LogDebug(() => $"Started checking cache for the '{downstreamUrlKey}' key."); var cached = _outputCache.Get(downStreamRequestCacheKey, options.Region); if (cached != null) { Logger.LogDebug(() => $"Cache entry exists for the '{downstreamUrlKey}' key."); var response = CreateHttpResponseMessage(cached); SetHttpResponseMessageThisRequest(httpContext, response); Logger.LogDebug(() => $"Finished returning of cached response for the '{downstreamUrlKey}' key."); return; } Logger.LogDebug(() => $"No response cached for the '{downstreamUrlKey}' key."); await _next.Invoke(httpContext); if (httpContext.Items.Errors().Count > 0) { Logger.LogDebug(() => $"There was a pipeline error for the '{downstreamUrlKey}' key."); return; } var downstreamResponse = httpContext.Items.DownstreamResponse(); cached = await CreateCachedResponse(downstreamResponse); var ttl = TimeSpan.FromSeconds(options.TtlSeconds); _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl); Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); } private static void SetHttpResponseMessageThisRequest(HttpContext context, DownstreamResponse response) => context.Items.UpsertDownstreamResponse(response); private static DownstreamResponse CreateHttpResponseMessage(CachedResponse cached) { var content = new MemoryStream(Convert.FromBase64String(cached.Body)); var streamContent = new StreamContent(content); foreach (var header in cached.ContentHeaders) { streamContent.Headers.TryAddWithoutValidation(header.Key, header.Value); } return new DownstreamResponse(streamContent, cached.StatusCode, cached.Headers.ToList(), cached.ReasonPhrase); } private static async Task CreateCachedResponse(DownstreamResponse response) { if (response == null) { return null; } var statusCode = response.StatusCode; var headers = response.Headers.ToDictionary(v => v.Key, v => v.Values); string body = null; if (response.Content != null) { var content = await response.Content.ReadAsByteArrayAsync(); body = Convert.ToBase64String(content); } var contentHeaders = response?.Content?.Headers.ToDictionary(v => v.Key, v => v.Value); var cached = new CachedResponse(statusCode, headers, body, contentHeaders, response.ReasonPhrase); return cached; } } ================================================ FILE: src/Ocelot/Claims/AddClaimsToRequest.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Claims; public class AddClaimsToRequest : IAddClaimsToRequest { private readonly IClaimsParser _claimsParser; public AddClaimsToRequest(IClaimsParser claimsParser) { _claimsParser = claimsParser; } public Response SetClaimsOnContext(List claimsToThings, HttpContext context) { foreach (var config in claimsToThings) { var value = _claimsParser.GetValue(context.User.Claims, config.NewKey, config.Delimiter, config.Index); if (value.IsError) { return new ErrorResponse(value.Errors); } var exists = context.User.Claims.FirstOrDefault(x => x.Type == config.ExistingKey); var identity = context.User.Identity as ClaimsIdentity; if (exists != null) { identity?.RemoveClaim(exists); } identity?.AddClaim(new Claim(config.ExistingKey, value.Data)); } return new OkResponse(); } } ================================================ FILE: src/Ocelot/Claims/IAddClaimsToRequest.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Claims; public interface IAddClaimsToRequest { Response SetClaimsOnContext(List claimsToThings, HttpContext context); } ================================================ FILE: src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Claims.Middleware; public class ClaimsToClaimsMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IAddClaimsToRequest _addClaimsToRequest; public ClaimsToClaimsMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IAddClaimsToRequest addClaimsToRequest) : base(loggerFactory.CreateLogger()) { _next = next; _addClaimsToRequest = addClaimsToRequest; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); if (downstreamRoute.ClaimsToClaims.Any()) { Logger.LogDebug("this route has instructions to convert claims to other claims"); var result = _addClaimsToRequest.SetClaimsOnContext(downstreamRoute.ClaimsToClaims, httpContext); if (result.IsError) { Logger.LogDebug("error converting claims to other claims, setting pipeline error"); httpContext.Items.UpsertErrors(result.Errors); return; } } await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/Configuration/AuthenticationOptions.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration; public sealed class AuthenticationOptions { public AuthenticationOptions() { AllowAnonymous = false; AllowedScopes = new(); AuthenticationProviderKeys = Array.Empty(); } public AuthenticationOptions(FileAuthenticationOptions options) { AllowAnonymous = options.AllowAnonymous ?? false; AllowedScopes = options.AllowedScopes ?? new(); AuthenticationProviderKeys = Merge(options.AuthenticationProviderKey, options.AuthenticationProviderKeys ?? Array.Empty()); } public AuthenticationOptions(List allowedScopes, string[] authenticationProviderKeys) { AllowAnonymous = false; AllowedScopes = allowedScopes ?? new(); AuthenticationProviderKeys = authenticationProviderKeys ?? Array.Empty(); } private static string[] Merge(string primaryKey, string[] keys) { if (primaryKey.IsEmpty()) { return keys; } List merged = new(1 + keys.Length) { primaryKey }; merged.AddRange(keys); return merged.ToArray(); } /// Allows anonymous authentication for route when global authentication options are used. /// if it is allowed; otherwise, . public bool AllowAnonymous { get; init; } public List AllowedScopes { get; init; } /// Multiple authentication schemes registered in DI services with appropriate authentication providers. /// The order in the collection matters: first successful authentication result wins. /// An array of values of the scheme names. public string[] AuthenticationProviderKeys { get; init; } public bool HasScheme => AuthenticationProviderKeys.Any(k => !k.IsEmpty()); } ================================================ FILE: src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Infrastructure.Extensions; using Ocelot.Values; namespace Ocelot.Configuration.Builder; public class DownstreamRouteBuilder { private AuthenticationOptions _authenticationOptions; private string _loadBalancerKey; private string _downstreamPathTemplate; private UpstreamPathTemplate _upstreamTemplatePattern; private HashSet _upstreamHttpMethod; private List _claimsToHeaders; private List _claimToClaims; private Dictionary _routeClaimRequirement; private List _claimToQueries; private List _claimToDownstreamPath; private string _requestIdHeaderKey; private CacheOptions _cacheOptions; private string _downstreamScheme; private LoadBalancerOptions _loadBalancerOptions; private QoSOptions _qosOptions; private HttpHandlerOptions _httpHandlerOptions; private RateLimitOptions _rateLimitOptions; private string _serviceName; private string _serviceNamespace; private List _upstreamHeaderFindAndReplace; private List _downstreamHeaderFindAndReplace; private readonly List _downstreamAddresses; private string _key; private List _delegatingHandlers; private List _addHeadersToDownstream; private List _addHeadersToUpstream; private bool _dangerousAcceptAnyServerCertificateValidator; private SecurityOptions _securityOptions; private string _downstreamHttpMethod; private Version _downstreamHttpVersion; private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; private MetadataOptions _metadataOptions; private int? _timeout; public DownstreamRouteBuilder() { _downstreamAddresses = new(); _delegatingHandlers = new(); _addHeadersToDownstream = new(); _addHeadersToUpstream = new(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) { _downstreamAddresses.AddRange(downstreamAddresses); return this; } public DownstreamRouteBuilder WithDownStreamHttpMethod(string method) { _downstreamHttpMethod = method; return this; } public DownstreamRouteBuilder WithLoadBalancerOptions(LoadBalancerOptions loadBalancerOptions) { _loadBalancerOptions = loadBalancerOptions; return this; } public DownstreamRouteBuilder WithDownstreamScheme(string downstreamScheme) { _downstreamScheme = downstreamScheme; return this; } public DownstreamRouteBuilder WithDownstreamPathTemplate(string input) { _downstreamPathTemplate = input; return this; } public DownstreamRouteBuilder WithUpstreamPathTemplate(UpstreamPathTemplate input) { _upstreamTemplatePattern = input; return this; } public DownstreamRouteBuilder WithUpstreamHttpMethod(IEnumerable methods) { _upstreamHttpMethod = methods.ToHttpMethods(); return this; } public DownstreamRouteBuilder WithRequestIdKey(string input) { _requestIdHeaderKey = input; return this; } public DownstreamRouteBuilder WithClaimsToHeaders(List input) { _claimsToHeaders = input; return this; } public DownstreamRouteBuilder WithClaimsToClaims(List input) { _claimToClaims = input; return this; } public DownstreamRouteBuilder WithRouteClaimsRequirement(Dictionary input) { _routeClaimRequirement = input; return this; } public DownstreamRouteBuilder WithClaimsToQueries(List input) { _claimToQueries = input; return this; } public DownstreamRouteBuilder WithClaimsToDownstreamPath(List input) { _claimToDownstreamPath = input; return this; } public DownstreamRouteBuilder WithCacheOptions(CacheOptions input) { _cacheOptions = input; return this; } public DownstreamRouteBuilder WithQosOptions(QoSOptions input) { _qosOptions = input; return this; } public DownstreamRouteBuilder WithLoadBalancerKey(string loadBalancerKey) { _loadBalancerKey = loadBalancerKey; return this; } public DownstreamRouteBuilder WithAuthenticationOptions(AuthenticationOptions authenticationOptions) { _authenticationOptions = authenticationOptions; return this; } public DownstreamRouteBuilder WithRateLimitOptions(RateLimitOptions input) { _rateLimitOptions = input; return this; } public DownstreamRouteBuilder WithHttpHandlerOptions(HttpHandlerOptions input) { _httpHandlerOptions = input; return this; } public DownstreamRouteBuilder WithServiceName(string serviceName) { _serviceName = serviceName; return this; } public DownstreamRouteBuilder WithServiceNamespace(string serviceNamespace) { _serviceNamespace = serviceNamespace; return this; } public DownstreamRouteBuilder WithUpstreamHeaderFindAndReplace(List upstreamHeaderFindAndReplace) { _upstreamHeaderFindAndReplace = upstreamHeaderFindAndReplace; return this; } public DownstreamRouteBuilder WithDownstreamHeaderFindAndReplace(List downstreamHeaderFindAndReplace) { _downstreamHeaderFindAndReplace = downstreamHeaderFindAndReplace; return this; } public DownstreamRouteBuilder WithKey(string key) { _key = key; return this; } public DownstreamRouteBuilder WithDelegatingHandlers(List delegatingHandlers) { _delegatingHandlers = delegatingHandlers; return this; } public DownstreamRouteBuilder WithAddHeadersToDownstream(List addHeadersToDownstream) { _addHeadersToDownstream = addHeadersToDownstream; return this; } public DownstreamRouteBuilder WithAddHeadersToUpstream(List addHeadersToUpstream) { _addHeadersToUpstream = addHeadersToUpstream; return this; } public DownstreamRouteBuilder WithDangerousAcceptAnyServerCertificateValidator(bool dangerousAcceptAnyServerCertificateValidator) { _dangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; return this; } public DownstreamRouteBuilder WithSecurityOptions(SecurityOptions securityOptions) { _securityOptions = securityOptions; return this; } public DownstreamRouteBuilder WithDownstreamHttpVersion(Version downstreamHttpVersion) { _downstreamHttpVersion = downstreamHttpVersion; return this; } public DownstreamRouteBuilder WithUpstreamHeaders(Dictionary input) { _upstreamHeaders = input; return this; } public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy downstreamHttpVersionPolicy) { _downstreamHttpVersionPolicy = downstreamHttpVersionPolicy; return this; } public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions) { _metadataOptions = metadataOptions; return this; } public DownstreamRouteBuilder WithTimeout(int? timeout) { _timeout = timeout; return this; } public DownstreamRoute Build() { return new DownstreamRoute( _key, _upstreamTemplatePattern, _upstreamHeaderFindAndReplace, _downstreamHeaderFindAndReplace, _downstreamAddresses, _serviceName, _serviceNamespace, _httpHandlerOptions, _qosOptions, _downstreamScheme, _requestIdHeaderKey, _cacheOptions, _loadBalancerOptions, _rateLimitOptions, _routeClaimRequirement, _claimToQueries, _claimsToHeaders, _claimToClaims, _claimToDownstreamPath, _authenticationOptions, new DownstreamPathTemplate(_downstreamPathTemplate), _loadBalancerKey, _delegatingHandlers, _addHeadersToDownstream, _addHeadersToUpstream, _dangerousAcceptAnyServerCertificateValidator, _securityOptions, _downstreamHttpMethod, _downstreamHttpVersion, _downstreamHttpVersionPolicy, _upstreamHeaders, _metadataOptions, _timeout); } } ================================================ FILE: src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs ================================================ using System.Globalization; namespace Ocelot.Configuration.Builder; public class MetadataOptionsBuilder { private string[] _separators; private char[] _trimChars; private StringSplitOptions _stringSplitOption; private NumberStyles _numberStyle; private CultureInfo _currentCulture; private IDictionary _metadata; public MetadataOptionsBuilder WithSeparators(string[] separators) { _separators = separators; return this; } public MetadataOptionsBuilder WithTrimChars(char[] trimChars) { _trimChars = trimChars; return this; } public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption) { _stringSplitOption = Enum.Parse(stringSplitOption); return this; } public MetadataOptionsBuilder WithNumberStyle(string numberStyle) { _numberStyle = Enum.Parse(numberStyle); return this; } public MetadataOptionsBuilder WithCurrentCulture(string currentCulture) { _currentCulture = CultureInfo.GetCultureInfo(currentCulture); return this; } public MetadataOptionsBuilder WithMetadata(IDictionary metadata) { _metadata = metadata; return this; } public MetadataOptions Build() { return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata); } } ================================================ FILE: src/Ocelot/Configuration/Builder/ServiceProviderConfigurationBuilder.cs ================================================ namespace Ocelot.Configuration.Builder; public class ServiceProviderConfigurationBuilder { private string _serviceDiscoveryProviderScheme; private string _serviceDiscoveryProviderHost; private int _serviceDiscoveryProviderPort; private string _type; private string _token; private string _configurationKey; private int _pollingInterval; private string _namespace; public ServiceProviderConfigurationBuilder WithScheme(string serviceDiscoveryProviderScheme) { _serviceDiscoveryProviderScheme = serviceDiscoveryProviderScheme; return this; } public ServiceProviderConfigurationBuilder WithHost(string serviceDiscoveryProviderHost) { _serviceDiscoveryProviderHost = serviceDiscoveryProviderHost; return this; } public ServiceProviderConfigurationBuilder WithPort(int serviceDiscoveryProviderPort) { _serviceDiscoveryProviderPort = serviceDiscoveryProviderPort; return this; } public ServiceProviderConfigurationBuilder WithType(string type) { _type = type; return this; } public ServiceProviderConfigurationBuilder WithToken(string token) { _token = token; return this; } public ServiceProviderConfigurationBuilder WithConfigurationKey(string configurationKey) { _configurationKey = configurationKey; return this; } public ServiceProviderConfigurationBuilder WithPollingInterval(int pollingInterval) { _pollingInterval = pollingInterval; return this; } public ServiceProviderConfigurationBuilder WithNamespace(string @namespace) { _namespace = @namespace; return this; } public ServiceProviderConfiguration Build() => new() { ConfigurationKey = _configurationKey, Host = _serviceDiscoveryProviderHost, Namespace = _namespace, PollingInterval = _pollingInterval, Port = _serviceDiscoveryProviderPort, Scheme = _serviceDiscoveryProviderScheme, Token = _token, Type = _type, }; } ================================================ FILE: src/Ocelot/Configuration/Builder/UpstreamPathTemplateBuilder.cs ================================================ using Ocelot.Values; namespace Ocelot.Configuration.Builder; public class UpstreamPathTemplateBuilder { private string _template; private int _priority; private bool _containsQueryString; private string _originalValue; public UpstreamPathTemplateBuilder WithTemplate(string template) { _template = template; return this; } public UpstreamPathTemplateBuilder WithPriority(int priority) { _priority = priority; return this; } public UpstreamPathTemplateBuilder WithContainsQueryString(bool containsQueryString) { _containsQueryString = containsQueryString; return this; } public UpstreamPathTemplateBuilder WithOriginalValue(string originalValue) { _originalValue = originalValue; return this; } public UpstreamPathTemplate Build() { return new UpstreamPathTemplate(_template, _priority, _containsQueryString, _originalValue); } } ================================================ FILE: src/Ocelot/Configuration/CacheOptions.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; using Ocelot.Request.Middleware; namespace Ocelot.Configuration; public class CacheOptions { public const int NoSeconds = 0; /// /// Separation of concerns between Ocelot's native caching control and the industry-standard Cache-Control header, which governs downstream caching behavior. /// public const string Oc_Cache_Control = "OC-Cache-Control"; internal CacheOptions() { } public CacheOptions(FileCacheOptions from, string defaultRegion) : this(from.TtlSeconds, from.Region.IfEmpty(defaultRegion), from.Header, from.EnableContentHashing) { } /// /// Initializes a new instance of the class. /// /// /// Internal defaults: /// /// The default value for is , but it is set to null for route-level configuration to allow global configuration usage. /// The default value for is 0. /// /// /// Time-to-live seconds. If not speciefied, zero value is used by default. /// The region of caching. /// The header name to control cached value. /// The switcher for content hashing. If not speciefied, false value is used by default. public CacheOptions(int? ttlSeconds, string region, string header, bool? enableContentHashing) { TtlSeconds = ttlSeconds ?? NoSeconds; Region = region; Header = header.IfEmpty(Oc_Cache_Control); EnableContentHashing = enableContentHashing ?? false; } /// Time-to-live seconds. /// Default value is 0. No caching by default. /// An value of seconds. public int TtlSeconds { get; } public string Region { get; } public string Header { get; } /// Enables MD5 hash calculation of the of the object. /// Default value is . No hashing by default. /// if hashing is enabled, otherwise it is . public bool EnableContentHashing { get; } public bool UseCache => TtlSeconds > NoSeconds; } ================================================ FILE: src/Ocelot/Configuration/ChangeTracking/IOcelotConfigurationChangeTokenSource.cs ================================================ using Microsoft.Extensions.Primitives; namespace Ocelot.Configuration.ChangeTracking; /// /// source which is activated when Ocelot's configuration is changed. /// public interface IOcelotConfigurationChangeTokenSource { IChangeToken ChangeToken { get; } void Activate(); } ================================================ FILE: src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeToken.cs ================================================ using Microsoft.Extensions.Primitives; namespace Ocelot.Configuration.ChangeTracking; public class OcelotConfigurationChangeToken : IChangeToken { public const double PollingIntervalSeconds = 1; private readonly ICollection _callbacks = new List(); private readonly object _lock = new(); private DateTime? _timeChanged; public IDisposable RegisterChangeCallback(Action callback, object state) { lock (_lock) { var wrapper = new CallbackWrapper(callback, state, _callbacks, _lock); _callbacks.Add(wrapper); return wrapper; } } public void Activate() { lock (_lock) { _timeChanged = DateTime.UtcNow; foreach (var wrapper in _callbacks) { wrapper.Invoke(); } } } // Token stays active for PollingIntervalSeconds after a change (could be parameterised) - otherwise HasChanged would be true forever. // Taking suggestions for better ways to reset HasChanged back to false. public bool HasChanged => _timeChanged.HasValue && (DateTime.UtcNow - _timeChanged.Value).TotalSeconds < PollingIntervalSeconds; public bool ActiveChangeCallbacks => true; private class CallbackWrapper : IDisposable { private readonly ICollection _callbacks; private readonly object _lock; public CallbackWrapper(Action callback, object state, ICollection callbacks, object @lock) { _callbacks = callbacks; _lock = @lock; Callback = callback; State = state; } public void Invoke() { Callback.Invoke(State); } public void Dispose() { lock (_lock) { _callbacks.Remove(this); } } public Action Callback { get; } public object State { get; } } } ================================================ FILE: src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSource.cs ================================================ using Microsoft.Extensions.Primitives; namespace Ocelot.Configuration.ChangeTracking; public class OcelotConfigurationChangeTokenSource : IOcelotConfigurationChangeTokenSource { private readonly OcelotConfigurationChangeToken _changeToken = new(); public IChangeToken ChangeToken => _changeToken; public void Activate() { _changeToken.Activate(); } } ================================================ FILE: src/Ocelot/Configuration/ChangeTracking/OcelotConfigurationMonitor.cs ================================================ using Microsoft.Extensions.Options; using Ocelot.Configuration.Repository; namespace Ocelot.Configuration.ChangeTracking; public class OcelotConfigurationMonitor : IOptionsMonitor { private readonly IInternalConfigurationRepository _repo; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; public OcelotConfigurationMonitor(IInternalConfigurationRepository repo, IOcelotConfigurationChangeTokenSource changeTokenSource) { ArgumentNullException.ThrowIfNull(repo); ArgumentNullException.ThrowIfNull(changeTokenSource); _repo = repo; _changeTokenSource = changeTokenSource; } public IInternalConfiguration Get(string name) { return _repo.Get().Data; } public IDisposable OnChange(Action listener) { ArgumentNullException.ThrowIfNull(listener); return _changeTokenSource.ChangeToken.RegisterChangeCallback(_ => listener(CurrentValue, string.Empty), null); } public IInternalConfiguration CurrentValue => _repo.Get().Data; } ================================================ FILE: src/Ocelot/Configuration/ClaimToThing.cs ================================================ namespace Ocelot.Configuration; public class ClaimToThing { public ClaimToThing(string existingKey, string newKey, string delimiter, int index) { NewKey = newKey; Delimiter = delimiter; Index = index; ExistingKey = existingKey; } public string ExistingKey { get; } public string NewKey { get; } public string Delimiter { get; } public int Index { get; } } ================================================ FILE: src/Ocelot/Configuration/Creator/AddHeader.cs ================================================ namespace Ocelot.Configuration.Creator; public class AddHeader { public AddHeader(string key, string value) { Key = key; Value = value; } public string Key { get; } public string Value { get; } } ================================================ FILE: src/Ocelot/Configuration/Creator/AggregatesCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class AggregatesCreator : IAggregatesCreator { private readonly IUpstreamTemplatePatternCreator _creator; private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator; public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator) { _creator = creator; _headerCreator = headerCreator; } public List Create(FileConfiguration fileConfiguration, IReadOnlyList routes) { return fileConfiguration.Aggregates .Select(aggregate => SetUpAggregateRoute(routes, aggregate, fileConfiguration.GlobalConfiguration)) .Where(aggregate => aggregate != null) .ToList(); } private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute aggregateRoute, FileGlobalConfiguration globalConfiguration) { var applicableRoutes = new List(); var allRoutes = routes.SelectMany(x => x.DownstreamRoute); var downstreamRoutes = aggregateRoute.RouteKeys.Select(routeKey => allRoutes.FirstOrDefault(q => q.Key == routeKey)); foreach (var downstreamRoute in downstreamRoutes) { if (downstreamRoute == null) { return null; } applicableRoutes.Add(downstreamRoute); } var upstreamTemplatePattern = _creator.Create(aggregateRoute); var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute); var upstreamHttpMethod = aggregateRoute.UpstreamHttpMethod.ToHttpMethods(); return new Route() { Aggregator = aggregateRoute.Aggregator, DownstreamRoute = applicableRoutes, DownstreamRouteConfig = aggregateRoute.RouteKeysConfig, UpstreamHeaderTemplates = upstreamHeaderTemplates, UpstreamHost = aggregateRoute.UpstreamHost, UpstreamHttpMethod = upstreamHttpMethod, UpstreamTemplatePattern = upstreamTemplatePattern, }; } } ================================================ FILE: src/Ocelot/Configuration/Creator/AuthenticationOptionsCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class AuthenticationOptionsCreator : IAuthenticationOptionsCreator { public AuthenticationOptions Create(FileAuthenticationOptions options) => new(options); public AuthenticationOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.AuthenticationOptions, globalConfiguration.AuthenticationOptions); } public AuthenticationOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.AuthenticationOptions, globalConfiguration.AuthenticationOptions); } protected virtual AuthenticationOptions Create(IRouteGrouping grouping, FileAuthenticationOptions options, FileGlobalAuthenticationOptions globalOptions) { ArgumentNullException.ThrowIfNull(grouping); bool isGlobal = globalOptions?.RouteKeys is null // undefined section or array option -> is global || globalOptions.RouteKeys.Count == 0 // empty collection -> is global || globalOptions.RouteKeys.Contains(grouping.Key); // this route is in the group if (options == null && globalOptions != null && isGlobal) { return new(globalOptions); } if (options != null && globalOptions == null) { return new(options); } if (options != null && globalOptions != null) { return isGlobal ? Merge(options, globalOptions) : new(options); } return new(); } protected virtual AuthenticationOptions Merge(FileAuthenticationOptions options, FileAuthenticationOptions globalOptions) { options.AllowAnonymous ??= globalOptions.AllowAnonymous; options.AllowedScopes ??= globalOptions.AllowedScopes; options.AuthenticationProviderKey = options.AuthenticationProviderKey.IfEmpty(globalOptions.AuthenticationProviderKey); if (!(options.AuthenticationProviderKeys?.Length > 0)) // TODO IfEmpty ICollection { options.AuthenticationProviderKeys = globalOptions.AuthenticationProviderKeys ?? []; } return new(options); } } ================================================ FILE: src/Ocelot/Configuration/Creator/CacheOptionsCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class CacheOptionsCreator : ICacheOptionsCreator { public CacheOptions Create(FileCacheOptions options) => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing); public CacheOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.FileCacheOptions ?? route.CacheOptions, globalConfiguration.CacheOptions, loadBalancingKey); } public CacheOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.CacheOptions, globalConfiguration.CacheOptions, loadBalancingKey); } protected virtual CacheOptions Create(IRouteGrouping grouping, FileCacheOptions options, FileGlobalCacheOptions globalOptions, string loadBalancingKey) { ArgumentNullException.ThrowIfNull(grouping); var group = globalOptions; var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global group.RouteKeys.Count == 0 || // empty collection -> is global group.RouteKeys.Contains(grouping.Key); // this route is in the group if (options == null && globalOptions != null && isGlobal) { return new(globalOptions, loadBalancingKey); } if (options != null && globalOptions == null) { return new(options, loadBalancingKey); } else if (options != null && globalOptions != null && !isGlobal) { return new(options, loadBalancingKey); } if (options != null && globalOptions != null && isGlobal) { return Merge(options, globalOptions, loadBalancingKey); } return new(); } protected virtual CacheOptions Merge(FileCacheOptions options, FileCacheOptions globalOptions, string defaultRegion) { var region = options.Region.IfEmpty(globalOptions.Region).IfEmpty(defaultRegion); var header = options.Header.IfEmpty(globalOptions.Header).IfEmpty(CacheOptions.Oc_Cache_Control); var ttlSeconds = options.TtlSeconds ?? globalOptions.TtlSeconds; var enableHashing = options.EnableContentHashing ?? globalOptions.EnableContentHashing; return new CacheOptions(ttlSeconds, region, header, enableHashing); } } ================================================ FILE: src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs ================================================ using Ocelot.Configuration.Parser; using Ocelot.Logging; namespace Ocelot.Configuration.Creator; public class ClaimsToThingCreator : IClaimsToThingCreator { private readonly IClaimToThingConfigurationParser _claimToThingConfigParser; private readonly IOcelotLogger _logger; public ClaimsToThingCreator(IClaimToThingConfigurationParser claimToThingConfigurationParser, IOcelotLoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); _claimToThingConfigParser = claimToThingConfigurationParser; } public List Create(Dictionary inputToBeParsed) { var claimsToThings = new List(); foreach (var input in inputToBeParsed) { var claimToThing = _claimToThingConfigParser.Extract(input.Key, input.Value); if (claimToThing.IsError) { _logger.LogDebug(() => $"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); } else { claimsToThings.Add(claimToThing.Data); } } return claimsToThings; } } ================================================ FILE: src/Ocelot/Configuration/Creator/ConfigurationCreator.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Administration; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class ConfigurationCreator : IConfigurationCreator { private readonly IAuthenticationOptionsCreator _authOptionsCreator; private readonly IServiceProviderConfigurationCreator _serviceProviderConfigCreator; private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IAdministrationPath _adminPath; private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; private readonly IMetadataCreator _metadataCreator; private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly ICacheOptionsCreator _cacheOptionsCreator; public ConfigurationCreator( IServiceProvider serviceProvider, IAuthenticationOptionsCreator authOptionsCreator, IServiceProviderConfigurationCreator serviceProviderConfigCreator, IQoSOptionsCreator qosOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator, IMetadataCreator metadataCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, ICacheOptionsCreator cacheOptionsCreator) { _adminPath = serviceProvider.GetService(); _authOptionsCreator = authOptionsCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _serviceProviderConfigCreator = serviceProviderConfigCreator; _qosOptionsCreator = qosOptionsCreator; _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; _metadataCreator = metadataCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _cacheOptionsCreator = cacheOptionsCreator; } public InternalConfiguration Create(FileConfiguration configuration, Route[] routes) { var adminPath = _adminPath?.Path; var globalConfiguration = configuration.GlobalConfiguration ?? new(); var authOptions = _authOptionsCreator.Create(globalConfiguration.AuthenticationOptions); var serviceProviderConfiguration = _serviceProviderConfigCreator.Create(globalConfiguration); var lbOptions = _loadBalancerOptionsCreator.Create(globalConfiguration.LoadBalancerOptions); var qosOptions = _qosOptionsCreator.Create(globalConfiguration.QoSOptions); var httpHandlerOptions = _httpHandlerOptionsCreator.Create(globalConfiguration.HttpHandlerOptions); var version = _versionCreator.Create(globalConfiguration.DownstreamHttpVersion); var versionPolicy = _versionPolicyCreator.Create(globalConfiguration.DownstreamHttpVersionPolicy); var metadataOptions = _metadataCreator.Create(null, globalConfiguration); var rateLimitOptions = _rateLimitOptionsCreator.Create(globalConfiguration); var cacheOptions = _cacheOptionsCreator.Create(globalConfiguration.CacheOptions); return new InternalConfiguration(routes) { AdministrationPath = adminPath, AuthenticationOptions = authOptions, CacheOptions = cacheOptions, DownstreamHttpVersion = version, DownstreamHttpVersionPolicy = versionPolicy, DownstreamScheme = globalConfiguration.DownstreamScheme, HttpHandlerOptions = httpHandlerOptions, LoadBalancerOptions = lbOptions, MetadataOptions = metadataOptions, QoSOptions = qosOptions, RateLimitOptions = rateLimitOptions, RequestId = globalConfiguration.RequestIdKey, ServiceProviderConfiguration = serviceProviderConfiguration, Timeout = globalConfiguration.Timeout, }; } } ================================================ FILE: src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; /// /// This class implements the interface. /// public class DefaultMetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration) { metadata ??= new Dictionary(); globalConfiguration.Metadata ??= new Dictionary(); var merged = new Dictionary(globalConfiguration.Metadata); foreach (var (key, value) in metadata) { merged[key] = value; } var options = globalConfiguration.MetadataOptions; return new MetadataOptionsBuilder() .WithMetadata(merged) .WithSeparators(options.Separators) .WithTrimChars(options.TrimChars) .WithStringSplitOption(options.StringSplitOption) .WithNumberStyle(options.NumberStyle) .WithCurrentCulture(options.CurrentCulture) .Build(); } } ================================================ FILE: src/Ocelot/Configuration/Creator/DownstreamAddressesCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class DownstreamAddressesCreator : IDownstreamAddressesCreator { public List Create(FileRoute route) { return route.DownstreamHostAndPorts.Select(hostAndPort => new DownstreamHostAndPort(hostAndPort.Host, hostAndPort.Port)).ToList(); } } ================================================ FILE: src/Ocelot/Configuration/Creator/DynamicRoutesCreator.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class DynamicRoutesCreator : IDynamicsCreator { private readonly IAuthenticationOptionsCreator _authOptionsCreator; private readonly ICacheOptionsCreator _cacheOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IMetadataCreator _metadataCreator; private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly IRouteKeyCreator _loadBalancerKeyCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; public DynamicRoutesCreator( IAuthenticationOptionsCreator authOptionsCreator, ICacheOptionsCreator cacheOptionsCreator, IHttpHandlerOptionsCreator handlerOptionsCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IMetadataCreator metadataCreator, IQoSOptionsCreator qosOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, IRouteKeyCreator loadBalancerKeyCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator) { _authOptionsCreator = authOptionsCreator; _cacheOptionsCreator = cacheOptionsCreator; _httpHandlerOptionsCreator = handlerOptionsCreator; _loadBalancerKeyCreator = loadBalancerKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _metadataCreator = metadataCreator; _qosOptionsCreator = qosOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; } public IReadOnlyList Create(FileConfiguration fileConfiguration) { Route CreateRoute(FileDynamicRoute route) => SetUpDynamicRoute(route, fileConfiguration.GlobalConfiguration); return fileConfiguration.DynamicRoutes .Select(CreateRoute) .ToArray(); } public virtual int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global) { int def = DownstreamRoute.DefaultTimeoutSeconds; return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; } private Route SetUpDynamicRoute(FileDynamicRoute dynamicRoute, FileGlobalConfiguration globalConfiguration) { // The old RateLimitRule property takes precedence over the new RateLimitOptions property for backward compatibility, thus, override forcibly if (dynamicRoute.RateLimitRule != null) { dynamicRoute.RateLimitOptions = dynamicRoute.RateLimitRule; } // Load balancing dependants var lbOptions = _loadBalancerOptionsCreator.Create(dynamicRoute, globalConfiguration); var lbKey = _loadBalancerKeyCreator.Create(dynamicRoute, lbOptions); var cacheOptions = _cacheOptionsCreator.Create(dynamicRoute, globalConfiguration, lbKey); var authOptions = _authOptionsCreator.Create(dynamicRoute, globalConfiguration); var version = _versionCreator.Create(dynamicRoute.DownstreamHttpVersion.IfEmpty(globalConfiguration.DownstreamHttpVersion)); var versionPolicy = _versionPolicyCreator.Create(dynamicRoute.DownstreamHttpVersionPolicy.IfEmpty(globalConfiguration.DownstreamHttpVersionPolicy)); var scheme = dynamicRoute.DownstreamScheme.IfEmpty(globalConfiguration.DownstreamScheme); var handlerOptions = _httpHandlerOptionsCreator.Create(dynamicRoute, globalConfiguration); var metadata = _metadataCreator.Create(dynamicRoute.Metadata, globalConfiguration); var qosOptions = _qosOptionsCreator.Create(dynamicRoute, globalConfiguration); var rlOptions = _rateLimitOptionsCreator.Create(dynamicRoute, globalConfiguration); var timeout = CreateTimeout(dynamicRoute, globalConfiguration); var downstreamRoute = new DownstreamRouteBuilder() .WithAuthenticationOptions(authOptions) .WithCacheOptions(cacheOptions) .WithDownstreamHttpVersion(version) .WithDownstreamHttpVersionPolicy(versionPolicy) .WithDownstreamScheme(scheme) .WithHttpHandlerOptions(handlerOptions) .WithLoadBalancerKey(lbKey) .WithLoadBalancerOptions(lbOptions) .WithMetadata(metadata) .WithQosOptions(qosOptions) .WithRateLimitOptions(rlOptions) .WithServiceName(dynamicRoute.ServiceName) .WithServiceNamespace(dynamicRoute.ServiceNamespace) .WithTimeout(timeout) .Build(); return new Route(true, downstreamRoute); // IsDynamic -> true } } ================================================ FILE: src/Ocelot/Configuration/Creator/FileInternalConfigurationCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using Ocelot.Responses; namespace Ocelot.Configuration.Creator; public class FileInternalConfigurationCreator : IInternalConfigurationCreator { private readonly IConfigurationValidator _configurationValidator; private readonly IConfigurationCreator _configCreator; private readonly IDynamicsCreator _dynamicsCreator; private readonly IRoutesCreator _routesCreator; private readonly IAggregatesCreator _aggregatesCreator; public FileInternalConfigurationCreator( IConfigurationValidator configurationValidator, IRoutesCreator routesCreator, IAggregatesCreator aggregatesCreator, IDynamicsCreator dynamicsCreator, IConfigurationCreator configCreator ) { _configCreator = configCreator; _dynamicsCreator = dynamicsCreator; _aggregatesCreator = aggregatesCreator; _routesCreator = routesCreator; _configurationValidator = configurationValidator; } public async Task> Create(FileConfiguration fileConfiguration) { var response = await _configurationValidator.IsValid(fileConfiguration); if (response.Data.IsError) { return new ErrorResponse(response.Data.Errors); } var routes = _routesCreator.Create(fileConfiguration); var aggregates = _aggregatesCreator.Create(fileConfiguration, routes); var dynamicRoute = _dynamicsCreator.Create(fileConfiguration); var mergedRoutes = routes .Union(aggregates) .Union(dynamicRoute) .ToArray(); var config = _configCreator.Create(fileConfiguration, mergedRoutes); return new OkResponse(config); } } ================================================ FILE: src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs ================================================ using Microsoft.Extensions.Options; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Header = System.Collections.Generic.KeyValuePair; namespace Ocelot.Configuration.Creator; public class HeaderFindAndReplaceCreator : IHeaderFindAndReplaceCreator { private readonly IPlaceholders _placeholders; private readonly IOcelotLogger _logger; private readonly FileGlobalConfiguration _globalConfiguration; public HeaderFindAndReplaceCreator(IPlaceholders placeholders, IOcelotLoggerFactory factory, IOptions global) { _placeholders = placeholders; _logger = factory.CreateLogger(); _globalConfiguration = global.Value; } public HeaderTransformations Create(FileRoute route) => Create(route, _globalConfiguration); public HeaderTransformations Create(FileRoute route, FileGlobalConfiguration global) { global ??= _globalConfiguration; var upstreamTransform = Merge(route.UpstreamHeaderTransform, global.UpstreamHeaderTransform); var (upstream, addHeadersToUpstream) = ProcessHeaders(upstreamTransform, nameof(route.UpstreamHeaderTransform)); var downstreamTransform = Merge(route.DownstreamHeaderTransform, global.DownstreamHeaderTransform); var (downstream, addHeadersToDownstream) = ProcessHeaders(downstreamTransform, nameof(route.DownstreamHeaderTransform)); return new HeaderTransformations(upstream, downstream, addHeadersToDownstream, addHeadersToUpstream); } /// Merge global Up/Downstream settings to the Route local ones. /// The Route local settings. /// Global default settings. /// An collection where T is . public static IEnumerable
Merge(IDictionary local, IDictionary global) { local ??= new Dictionary(); global ??= new Dictionary(); var toAdd = global.ExceptBy(local.Keys, x => x.Key); // Winning strategy: the route Transform-value wins over global one return local.Union(toAdd); } private (List StreamHeaders, List AddHeaders) ProcessHeaders(IEnumerable
headerTransform, string propertyName) { var addHeaders = new List(); var streamHeaders = new List(); foreach (var input in headerTransform) { if (input.Value.Contains(HeaderFindAndReplace.Comma)) { var hAndr = Map(input); if (hAndr != null) { streamHeaders.Add(hAndr); } else { _logger.LogWarning(() => $"Unable to add {propertyName} {input}"); } } else { addHeaders.Add(new AddHeader(input.Key, input.Value)); } } return (streamHeaders, addHeaders); } private HeaderFindAndReplace Map(Header input) { var findAndReplace = input.Value.Split(HeaderFindAndReplace.Comma); var replace = findAndReplace[1].TrimStart(); var startOfPlaceholder = replace.IndexOf(Placeholders.OpeningBrace, StringComparison.Ordinal); if (startOfPlaceholder > -1) { var endOfPlaceholder = replace.IndexOf(Placeholders.ClosingBrace, startOfPlaceholder); var placeholder = replace.Substring(startOfPlaceholder, endOfPlaceholder - startOfPlaceholder + 1); var value = _placeholders.Get(placeholder); if (value.IsError) { _logger.LogWarning(() => $"{nameof(HeaderFindAndReplace)} was not mapped from {input} due to {value.Errors.ToErrorString()}"); return null; } replace = replace.Replace(placeholder, value.Data); } return new(input.Key, findAndReplace[0], replace, 0); } } ================================================ FILE: src/Ocelot/Configuration/Creator/HeaderTransformations.cs ================================================ namespace Ocelot.Configuration.Creator; public class HeaderTransformations { public HeaderTransformations( List upstream, List downstream, List addHeaderToDownstream, List addHeaderToUpstream) { AddHeadersToDownstream = addHeaderToDownstream; AddHeadersToUpstream = addHeaderToUpstream; Upstream = upstream; Downstream = downstream; } public List Upstream { get; } public List Downstream { get; } public List AddHeadersToDownstream { get; } public List AddHeadersToUpstream { get; } } ================================================ FILE: src/Ocelot/Configuration/Creator/HttpHandlerOptionsCreator.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.Logging; namespace Ocelot.Configuration.Creator; public class HttpHandlerOptionsCreator : IHttpHandlerOptionsCreator { private readonly IOcelotTracer _tracer; public HttpHandlerOptionsCreator(IServiceProvider services) => _tracer = services.GetService(); public HttpHandlerOptions Create(FileHttpHandlerOptions options) { options ??= new(); var hasTracer = _tracer != null; return new(options, hasTracer); } public HttpHandlerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.HttpHandlerOptions, globalConfiguration.HttpHandlerOptions); } public HttpHandlerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.HttpHandlerOptions, globalConfiguration.HttpHandlerOptions); } protected virtual HttpHandlerOptions Create(IRouteGrouping grouping, FileHttpHandlerOptions options, FileGlobalHttpHandlerOptions globalOptions) { ArgumentNullException.ThrowIfNull(grouping); var group = globalOptions; var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global group.RouteKeys.Count == 0 || // empty collection -> is global group.RouteKeys.Contains(grouping.Key); // this route is in the group var hasTracer = _tracer != null; if (options == null && globalOptions != null && isGlobal) { return new(globalOptions, hasTracer); } if (options != null && globalOptions == null) { return new(options, hasTracer); } else if (options != null && globalOptions != null && !isGlobal) { return new(options, hasTracer); } if (options != null && globalOptions != null && isGlobal) { return Merge(options, globalOptions); } return new(); } protected virtual HttpHandlerOptions Merge(FileHttpHandlerOptions options, FileHttpHandlerOptions globalOptions) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(globalOptions); options.AllowAutoRedirect ??= globalOptions.AllowAutoRedirect ?? false; options.MaxConnectionsPerServer ??= globalOptions.MaxConnectionsPerServer ?? int.MaxValue; options.PooledConnectionLifetimeSeconds ??= globalOptions.PooledConnectionLifetimeSeconds ?? HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds; options.UseCookieContainer ??= globalOptions.UseCookieContainer ?? false; options.UseProxy ??= globalOptions.UseProxy ?? false; options.UseTracing ??= globalOptions.UseTracing ?? false; var useTracing = _tracer != null && options.UseTracing.Value; return new(options, useTracing); } } ================================================ FILE: src/Ocelot/Configuration/Creator/HttpVersionCreator.cs ================================================ namespace Ocelot.Configuration.Creator; public class HttpVersionCreator : IVersionCreator { public Version Create(string downstreamHttpVersion) { if (!Version.TryParse(downstreamHttpVersion, out var version)) { version = new Version(1, 1); } return version; } } ================================================ FILE: src/Ocelot/Configuration/Creator/HttpVersionPolicyCreator.cs ================================================ namespace Ocelot.Configuration.Creator; /// /// Default implementation of the interface. /// public class HttpVersionPolicyCreator : IVersionPolicyCreator { /// /// Creates a by a string. /// /// The string representation of the version policy. /// An enumeration value. public HttpVersionPolicy Create(string downstreamHttpVersionPolicy) => downstreamHttpVersionPolicy switch { VersionPolicies.RequestVersionExact => HttpVersionPolicy.RequestVersionExact, VersionPolicies.RequestVersionOrHigher => HttpVersionPolicy.RequestVersionOrHigher, VersionPolicies.RequestVersionOrLower => HttpVersionPolicy.RequestVersionOrLower, _ => HttpVersionPolicy.RequestVersionOrLower, }; } ================================================ FILE: src/Ocelot/Configuration/Creator/IAggregatesCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IAggregatesCreator { List Create(FileConfiguration fileConfiguration, IReadOnlyList routes); } ================================================ FILE: src/Ocelot/Configuration/Creator/IAuthenticationOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IAuthenticationOptionsCreator { AuthenticationOptions Create(FileAuthenticationOptions options); AuthenticationOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); AuthenticationOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/ICacheOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; /// /// This interface is used to create cache options. /// public interface ICacheOptionsCreator { CacheOptions Create(FileCacheOptions options); CacheOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey); CacheOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration, string loadBalancingKey); } ================================================ FILE: src/Ocelot/Configuration/Creator/IClaimsToThingCreator.cs ================================================ namespace Ocelot.Configuration.Creator; public interface IClaimsToThingCreator { List Create(Dictionary thingsBeingAdded); } ================================================ FILE: src/Ocelot/Configuration/Creator/IConfigurationCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IConfigurationCreator { InternalConfiguration Create(FileConfiguration configuration, Route[] routes); } ================================================ FILE: src/Ocelot/Configuration/Creator/IDownstreamAddressesCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IDownstreamAddressesCreator { List Create(FileRoute route); } ================================================ FILE: src/Ocelot/Configuration/Creator/IDynamicsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IDynamicsCreator { IReadOnlyList Create(FileConfiguration fileConfiguration); /// /// Creates a timeout value for a given file route based on the global configuration. /// /// The file route for which to create the timeout. /// The global configuration to use for creating the timeout. /// The timeout value in seconds. int CreateTimeout(FileDynamicRoute route, FileGlobalConfiguration global); } ================================================ FILE: src/Ocelot/Configuration/Creator/IHeaderFindAndReplaceCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IHeaderFindAndReplaceCreator { HeaderTransformations Create(FileRoute fileRoute); HeaderTransformations Create(FileRoute route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IHttpHandlerOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IHttpHandlerOptionsCreator { HttpHandlerOptions Create(FileHttpHandlerOptions options); HttpHandlerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); HttpHandlerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IInternalConfigurationCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Responses; namespace Ocelot.Configuration.Creator; public interface IInternalConfigurationCreator { Task> Create(FileConfiguration fileConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/ILoadBalancerOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface ILoadBalancerOptionsCreator { LoadBalancerOptions Create(FileLoadBalancerOptions options); LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IMetadataCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; /// /// This interface describes the creation of metadata options. /// public interface IMetadataCreator { MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IQoSOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IQoSOptionsCreator { QoSOptions Create(FileQoSOptions options); QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration); QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IRateLimitOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IRateLimitOptionsCreator { RateLimitOptions Create(FileGlobalConfiguration globalConfiguration); RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IRequestIdKeyCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IRequestIdKeyCreator { string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IRouteKeyCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IRouteKeyCreator { string Create(FileRoute route, LoadBalancerOptions loadBalancing); string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing); string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing); } ================================================ FILE: src/Ocelot/Configuration/Creator/IRoutesCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IRoutesCreator { IReadOnlyList Create(FileConfiguration fileConfiguration); /// /// Creates a timeout value for a given file route based on the global configuration. /// /// The file route for which to create the timeout. /// The global configuration to use for creating the timeout. /// The timeout value in seconds. int CreateTimeout(FileRoute route, FileGlobalConfiguration global); } ================================================ FILE: src/Ocelot/Configuration/Creator/ISecurityOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface ISecurityOptionsCreator { SecurityOptions Create(FileSecurityOptions securityOptions, FileGlobalConfiguration global); } ================================================ FILE: src/Ocelot/Configuration/Creator/IServiceProviderConfigurationCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public interface IServiceProviderConfigurationCreator { ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Creator/IUpstreamHeaderTemplatePatternCreator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Values; namespace Ocelot.Configuration.Creator; /// /// Ocelot feature: Routing based on request header. /// public interface IUpstreamHeaderTemplatePatternCreator { /// /// Creates upstream templates based on route headers. /// /// The route info. /// An object where TKey is , TValue is . IDictionary Create(IRouteUpstream route); IDictionary Create(IHeaderDictionary upstreamHeaderTemplates, bool routeIsCaseSensitive); } ================================================ FILE: src/Ocelot/Configuration/Creator/IUpstreamTemplatePatternCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Values; namespace Ocelot.Configuration.Creator; public interface IUpstreamTemplatePatternCreator { UpstreamPathTemplate Create(IRouteUpstream route); } ================================================ FILE: src/Ocelot/Configuration/Creator/IVersionCreator.cs ================================================ namespace Ocelot.Configuration.Creator; public interface IVersionCreator { Version Create(string downstreamHttpVersion); } ================================================ FILE: src/Ocelot/Configuration/Creator/IVersionPolicyCreator.cs ================================================ namespace Ocelot.Configuration.Creator; /// /// Defines conversions from version policy strings to enumeration values. /// public interface IVersionPolicyCreator { /// /// Creates a by a string. /// /// The string representation of the version policy. /// An enumeration value. HttpVersionPolicy Create(string downstreamHttpVersionPolicy); } ================================================ FILE: src/Ocelot/Configuration/Creator/LoadBalancerOptionsCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class LoadBalancerOptionsCreator : ILoadBalancerOptionsCreator { public LoadBalancerOptions Create(FileLoadBalancerOptions options) => new(options); public LoadBalancerOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions); } public LoadBalancerOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.LoadBalancerOptions, globalConfiguration.LoadBalancerOptions); } protected virtual LoadBalancerOptions Create(IRouteGrouping grouping, FileLoadBalancerOptions options, FileGlobalLoadBalancerOptions globalOptions) { ArgumentNullException.ThrowIfNull(grouping); var group = globalOptions; var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global group.RouteKeys.Count == 0 || // empty collection -> is global group.RouteKeys.Contains(grouping.Key); // this route is in the group if (options == null && globalOptions != null && isGlobal) { return new(globalOptions); } if (options != null && globalOptions == null) { return new(options); } else if (options != null && globalOptions != null && !isGlobal) { return new(options); } if (options != null && globalOptions != null && isGlobal) { return Merge(options, globalOptions); } return new(); } protected virtual LoadBalancerOptions Merge(FileLoadBalancerOptions options, FileLoadBalancerOptions globalOptions) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(globalOptions); options.Type = options.Type.IfEmpty(globalOptions.Type); options.Key = options.Key.IfEmpty(globalOptions.Key); options.Expiry ??= globalOptions.Expiry; return new(options); } } ================================================ FILE: src/Ocelot/Configuration/Creator/QoSOptionsCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class QoSOptionsCreator : IQoSOptionsCreator { public QoSOptions Create(FileQoSOptions options) => new(options ?? new()); public QoSOptions Create(FileRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.QoSOptions, globalConfiguration.QoSOptions); } public QoSOptions Create(FileDynamicRoute route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); return Create(route, route.QoSOptions, globalConfiguration.QoSOptions); } protected virtual QoSOptions Create(IRouteGrouping grouping, FileQoSOptions options, FileGlobalQoSOptions globalOptions) { ArgumentNullException.ThrowIfNull(grouping); bool isGlobal = globalOptions?.RouteKeys is null // undefined section or array option -> is global || globalOptions.RouteKeys.Count == 0 // empty collection -> is global || globalOptions.RouteKeys.Contains(grouping.Key); // this route is in the group if (options == null && globalOptions != null && isGlobal) { return new(globalOptions); } if (options != null && globalOptions == null) { return new(options); } if (options != null && globalOptions != null) { return isGlobal ? Merge(options, globalOptions) : new(options); } return new(); } protected virtual QoSOptions Merge(FileQoSOptions options, FileQoSOptions global) { options ??= new(); global ??= new(); options.DurationOfBreak ??= global.DurationOfBreak; options.BreakDuration ??= global.BreakDuration; options.ExceptionsAllowedBeforeBreaking ??= global.ExceptionsAllowedBeforeBreaking; options.MinimumThroughput ??= global.MinimumThroughput; options.FailureRatio ??= global.FailureRatio; options.SamplingDuration ??= global.SamplingDuration; options.TimeoutValue ??= global.TimeoutValue; options.Timeout ??= global.Timeout; return new(options); } } ================================================ FILE: src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class RateLimitOptionsCreator : IRateLimitOptionsCreator { public RateLimitOptionsCreator() { } public RateLimitOptions Create(FileGlobalConfiguration globalConfiguration) => globalConfiguration.RateLimitOptions != null ? new(globalConfiguration.RateLimitOptions) : new(false); public RateLimitOptions Create(IRouteRateLimiting route, FileGlobalConfiguration globalConfiguration) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(globalConfiguration); var rule = route.RateLimitOptions; var globalOptions = globalConfiguration.RateLimitOptions; var group = globalOptions as IRouteGroup; var isGlobal = group?.RouteKeys is null || // undefined section or array option -> is global group.RouteKeys.Count == 0 || // empty collection -> is global group.RouteKeys.Contains(route.Key); // this route is in the group if (rule?.EnableRateLimiting == false || (isGlobal && globalOptions?.EnableRateLimiting == false)) { return new(false); } // By Client's Header rule merging if (rule == null && globalOptions != null && isGlobal) { return new(globalOptions); } if (rule != null && (globalOptions == null || (globalOptions != null && !isGlobal))) { return new(rule); } if (rule != null && globalOptions != null && isGlobal) { return MergeHeaderRules(rule, globalOptions); } return new(false); } protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule rule, FileRateLimitByHeaderRule globalRule) { ArgumentNullException.ThrowIfNull(rule); ArgumentNullException.ThrowIfNull(globalRule); rule.ClientIdHeader = rule.ClientIdHeader.IfEmpty(globalRule.ClientIdHeader.IfEmpty(RateLimitOptions.DefaultClientHeader)); rule.ClientWhitelist ??= globalRule.ClientWhitelist ?? []; if (!(rule.ClientWhitelist?.Count > 0)) // TODO IfEmpty ICollection { rule.ClientWhitelist = globalRule.ClientWhitelist ?? []; } // Final merging of EnableHeaders is implemented in the constructor rule.DisableRateLimitHeaders ??= globalRule.DisableRateLimitHeaders; rule.EnableHeaders ??= globalRule.EnableHeaders; rule.EnableRateLimiting ??= globalRule.EnableRateLimiting ?? true; // Final merging of StatusCode is implemented in the constructor rule.HttpStatusCode ??= globalRule.HttpStatusCode; rule.StatusCode ??= globalRule.StatusCode; // Final merging of QuotaMessage is implemented in the constructor rule.QuotaExceededMessage = rule.QuotaExceededMessage.IfEmpty(globalRule.QuotaExceededMessage); rule.QuotaMessage = rule.QuotaMessage.IfEmpty(globalRule.QuotaMessage); // Final merging of KeyPrefix is implemented in the constructor rule.RateLimitCounterPrefix = rule.RateLimitCounterPrefix.IfEmpty(globalRule.RateLimitCounterPrefix); rule.KeyPrefix = rule.KeyPrefix.IfEmpty(globalRule.KeyPrefix); rule.Period = rule.Period.IfEmpty(globalRule.Period.IfEmpty(RateLimitRule.DefaultPeriod)); // Final merging of Wait is implemented in the constructor rule.PeriodTimespan ??= globalRule.PeriodTimespan; rule.Wait = rule.Wait.IfEmpty(globalRule.Wait.IfEmpty(RateLimitRule.ZeroWait)); rule.Limit ??= globalRule.Limit ?? RateLimitRule.ZeroLimit; return new(rule); } } ================================================ FILE: src/Ocelot/Configuration/Creator/RequestIdKeyCreator.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class RequestIdKeyCreator : IRequestIdKeyCreator { public string Create(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) { var routeId = !string.IsNullOrEmpty(fileRoute.RequestIdKey); var requestIdKey = routeId ? fileRoute.RequestIdKey : globalConfiguration.RequestIdKey; return requestIdKey; } } ================================================ FILE: src/Ocelot/Configuration/Creator/RouteKeyCreator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer.Balancers; namespace Ocelot.Configuration.Creator; public class RouteKeyCreator : IRouteKeyCreator { public const char Separator = '|'; public const char Dot = DiscoveryDownstreamRouteFinder.Dot; /// /// Creates the unique key based on the route properties for load balancing etc. /// /// /// Key template: UpstreamHttpMethod|UpstreamPathTemplate|UpstreamHost|DownstreamHostAndPorts|ServiceNamespace|ServiceName|LoadBalancerType|LoadBalancerKey. /// /// The route object. /// Final options for load balancing. /// A object containing the key. public string Create(FileRoute route, LoadBalancerOptions loadBalancing) { if (TryStickySession(loadBalancing, out var stickySessionKey)) { return stickySessionKey; } var keyBuilder = new StringBuilder() .AppendNext(route.UpstreamHttpMethod.Csv()) // required .AppendNext(route.UpstreamPathTemplate) // required .AppendNext(route.UpstreamHost.IfEmpty("no-host")) // optional... .AppendNext(route.DownstreamHostAndPorts.Select(AsString).Csv().IfEmpty("no-host-and-port")) .AppendNext(route.ServiceNamespace.IfEmpty("no-svc-ns")) .AppendNext(route.ServiceName.IfEmpty("no-svc-name")) .AppendNext(loadBalancing.Type.IfEmpty("no-lb-type")) .AppendNext(loadBalancing.Key.IfEmpty("no-lb-key")); return keyBuilder.ToString(); } public string Create(FileDynamicRoute route, LoadBalancerOptions loadBalancing) { if (TryStickySession(loadBalancing, out var stickySessionKey)) { return stickySessionKey; } // it should be constructed in upper contexts return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key : Create(route.ServiceNamespace, route.ServiceName, loadBalancing); } public string Create(string serviceNamespace, string serviceName, LoadBalancerOptions loadBalancing) { if (TryStickySession(loadBalancing, out var stickySessionKey)) { return stickySessionKey; } return !loadBalancing.Key.IsEmpty() ? loadBalancing.Key : string.Join(Dot, serviceNamespace, serviceName); // upstreamHttpMethod ? } protected virtual bool TryStickySession(LoadBalancerOptions loadBalancing, out string stickySessionKey) { bool isStickySession = nameof(CookieStickySessions).Equals(loadBalancing.Type, StringComparison.OrdinalIgnoreCase) && loadBalancing.Key.Length > 0; stickySessionKey = isStickySession ? $"{nameof(CookieStickySessions)}:{loadBalancing.Key}" : string.Empty; return isStickySession; } public static string AsString(FileHostAndPort host) => host?.ToString(); } internal static class RouteKeyCreatorHelpers { /// Helper function to append a string to the key builder, separated by a pipe. /// The builder of the key. /// The next word to add. /// The character used to separate entries. /// The reference to the builder. public static StringBuilder AppendNext(this StringBuilder builder, string next, char separator = RouteKeyCreator.Separator) => StringBuilderExtensions.AppendNext(builder, next, separator); } ================================================ FILE: src/Ocelot/Configuration/Creator/SecurityOptionsCreator.cs ================================================ using NetTools; // using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class SecurityOptionsCreator : ISecurityOptionsCreator { public SecurityOptions Create(FileSecurityOptions securityOptions, FileGlobalConfiguration global) { var options = securityOptions.IsEmpty() ? global.SecurityOptions : securityOptions; var allowedIPs = options.IPAllowedList.SelectMany(Parse) .ToArray(); var blockedIPs = options.IPBlockedList.SelectMany(Parse) .Except(options.ExcludeAllowedFromBlocked ? allowedIPs : Enumerable.Empty()) .ToArray(); return new(allowedIPs, blockedIPs); } private static string[] Parse(string ipValue) { if (IPAddressRange.TryParse(ipValue, out var range)) { return range.Select(ip => ip.ToString()).ToArray(); } return Array.Empty(); } } ================================================ FILE: src/Ocelot/Configuration/Creator/ServiceProviderConfigurationCreator.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class ServiceProviderConfigurationCreator : IServiceProviderConfigurationCreator { public ServiceProviderConfiguration Create(FileGlobalConfiguration globalConfiguration) { var port = globalConfiguration?.ServiceDiscoveryProvider?.Port ?? 0; var scheme = globalConfiguration?.ServiceDiscoveryProvider?.Scheme ?? "http"; var host = globalConfiguration?.ServiceDiscoveryProvider?.Host ?? "localhost"; var type = !string.IsNullOrEmpty(globalConfiguration?.ServiceDiscoveryProvider?.Type) ? globalConfiguration?.ServiceDiscoveryProvider?.Type : "consul"; var pollingInterval = globalConfiguration?.ServiceDiscoveryProvider?.PollingInterval ?? 0; var k8snamespace = globalConfiguration?.ServiceDiscoveryProvider?.Namespace ?? string.Empty; return new ServiceProviderConfigurationBuilder() .WithScheme(scheme) .WithHost(host) .WithPort(port) .WithType(type) .WithToken(globalConfiguration?.ServiceDiscoveryProvider?.Token) .WithConfigurationKey(globalConfiguration?.ServiceDiscoveryProvider?.ConfigurationKey) .WithPollingInterval(pollingInterval) .WithNamespace(k8snamespace) .Build(); } } ================================================ FILE: src/Ocelot/Configuration/Creator/StaticRoutesCreator.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Creator; public class StaticRoutesCreator : IRoutesCreator { private readonly ILoadBalancerOptionsCreator _loadBalancerOptionsCreator; private readonly IClaimsToThingCreator _claimsToThingCreator; private readonly IAuthenticationOptionsCreator _authOptionsCreator; private readonly IUpstreamTemplatePatternCreator _upstreamTemplatePatternCreator; private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; private readonly IRequestIdKeyCreator _requestIdKeyCreator; private readonly IQoSOptionsCreator _qosOptionsCreator; private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly ICacheOptionsCreator _cacheOptionsCreator; private readonly IHttpHandlerOptionsCreator _httpHandlerOptionsCreator; private readonly IHeaderFindAndReplaceCreator _headerFAndRCreator; private readonly IDownstreamAddressesCreator _downstreamAddressesCreator; private readonly IRouteKeyCreator _routeKeyCreator; private readonly ISecurityOptionsCreator _securityOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; private readonly IMetadataCreator _metadataCreator; public StaticRoutesCreator( IClaimsToThingCreator claimsToThingCreator, IAuthenticationOptionsCreator authOptionsCreator, IUpstreamTemplatePatternCreator upstreamTemplatePatternCreator, IRequestIdKeyCreator requestIdKeyCreator, IQoSOptionsCreator qosOptionsCreator, IRateLimitOptionsCreator rateLimitOptionsCreator, ICacheOptionsCreator cacheOptionsCreator, IHttpHandlerOptionsCreator httpHandlerOptionsCreator, IHeaderFindAndReplaceCreator headerFAndRCreator, IDownstreamAddressesCreator downstreamAddressesCreator, ILoadBalancerOptionsCreator loadBalancerOptionsCreator, IRouteKeyCreator routeKeyCreator, ISecurityOptionsCreator securityOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator, IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, IMetadataCreator metadataCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _downstreamAddressesCreator = downstreamAddressesCreator; _headerFAndRCreator = headerFAndRCreator; _cacheOptionsCreator = cacheOptionsCreator; _rateLimitOptionsCreator = rateLimitOptionsCreator; _requestIdKeyCreator = requestIdKeyCreator; _upstreamTemplatePatternCreator = upstreamTemplatePatternCreator; _authOptionsCreator = authOptionsCreator; _claimsToThingCreator = claimsToThingCreator; _qosOptionsCreator = qosOptionsCreator; _httpHandlerOptionsCreator = httpHandlerOptionsCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; _securityOptionsCreator = securityOptionsCreator; _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; _metadataCreator = metadataCreator; } public IReadOnlyList Create(FileConfiguration fileConfiguration) { Route CreateRoute(FileRoute route) => SetUpRoute(route, SetUpDownstreamRoute(route, fileConfiguration.GlobalConfiguration)); return fileConfiguration.Routes .Select(CreateRoute) .ToArray(); } public virtual int CreateTimeout(FileRoute route, FileGlobalConfiguration global) { int def = DownstreamRoute.DefaultTimeoutSeconds; return route.Timeout.Positive(def) ?? global.Timeout.Positive(def) ?? def; } private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConfiguration globalConfiguration) { var requestIdKey = _requestIdKeyCreator.Create(fileRoute, globalConfiguration); var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); var authOptions = _authOptionsCreator.Create(fileRoute, globalConfiguration); var claimsToHeaders = _claimsToThingCreator.Create(fileRoute.AddHeadersToRequest); var claimsToClaims = _claimsToThingCreator.Create(fileRoute.AddClaimsToRequest); var claimsToQueries = _claimsToThingCreator.Create(fileRoute.AddQueriesToRequest); var claimsToDownstreamPath = _claimsToThingCreator.Create(fileRoute.ChangeDownstreamPathTemplate); var qosOptions = _qosOptionsCreator.Create(fileRoute, globalConfiguration); var rateLimitOption = _rateLimitOptionsCreator.Create(fileRoute, globalConfiguration); var httpHandlerOptions = _httpHandlerOptionsCreator.Create(fileRoute, globalConfiguration); var hAndRs = _headerFAndRCreator.Create(fileRoute, globalConfiguration); var downstreamAddresses = _downstreamAddressesCreator.Create(fileRoute); var lbOptions = _loadBalancerOptionsCreator.Create(fileRoute, globalConfiguration); var lbKey = _routeKeyCreator.Create(fileRoute, lbOptions); var securityOptions = _securityOptionsCreator.Create(fileRoute.SecurityOptions, globalConfiguration); var downstreamHttpVersion = _versionCreator.Create(fileRoute.DownstreamHttpVersion); var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy); var cacheOptions = _cacheOptionsCreator.Create(fileRoute, globalConfiguration, lbKey); var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration); var route = new DownstreamRouteBuilder() .WithAddHeadersToDownstream(hAndRs.AddHeadersToDownstream) .WithAddHeadersToUpstream(hAndRs.AddHeadersToUpstream) .WithAuthenticationOptions(authOptions) .WithCacheOptions(cacheOptions) .WithClaimsToClaims(claimsToClaims) .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithClaimsToHeaders(claimsToHeaders) .WithClaimsToQueries(claimsToQueries) .WithDangerousAcceptAnyServerCertificateValidator(fileRoute.DangerousAcceptAnyServerCertificateValidator) .WithDelegatingHandlers(fileRoute.DelegatingHandlers) .WithDownstreamAddresses(downstreamAddresses) .WithDownstreamHeaderFindAndReplace(hAndRs.Downstream) .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) .WithDownstreamHttpVersion(downstreamHttpVersion) .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) .WithDownstreamScheme(fileRoute.DownstreamScheme) .WithHttpHandlerOptions(httpHandlerOptions) .WithKey(fileRoute.Key) .WithLoadBalancerKey(lbKey) .WithLoadBalancerOptions(lbOptions) .WithMetadata(metadata) .WithQosOptions(qosOptions) .WithRateLimitOptions(rateLimitOption) .WithRequestIdKey(requestIdKey) .WithRouteClaimsRequirement(fileRoute.RouteClaimsRequirement) .WithSecurityOptions(securityOptions) .WithServiceName(fileRoute.ServiceName) .WithServiceNamespace(fileRoute.ServiceNamespace) .WithTimeout(CreateTimeout(fileRoute, globalConfiguration)) .WithUpstreamHeaderFindAndReplace(hAndRs.Upstream) .WithUpstreamHttpMethod(fileRoute.UpstreamHttpMethod.ToList()) .WithUpstreamPathTemplate(upstreamTemplatePattern) .Build(); return route; } private Route SetUpRoute(FileRoute fileRoute, DownstreamRoute downstreamRoute) { var upstreamTemplatePattern = _upstreamTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamPathTemplate var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(fileRoute); // TODO It should be downstreamRoute.UpstreamHeaders var upstreamHttpMethods = fileRoute.UpstreamHttpMethod.ToHttpMethods(); return new Route(downstreamRoute) { UpstreamHeaderTemplates = upstreamHeaderTemplates, // downstreamRoute.UpstreamHeaders UpstreamHost = fileRoute.UpstreamHost, UpstreamHttpMethod = upstreamHttpMethods, UpstreamTemplatePattern = upstreamTemplatePattern, }; } } ================================================ FILE: src/Ocelot/Configuration/Creator/UpstreamHeaderTemplatePatternCreator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Values; namespace Ocelot.Configuration.Creator; /// /// Default creator of upstream templates based on route headers. /// /// Ocelot feature: Routing based on request header. public partial class UpstreamHeaderTemplatePatternCreator : IUpstreamHeaderTemplatePatternCreator { [GeneratedRegex(@"(\{header:.*?\})", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")] private static partial Regex RegexPlaceholders(); public IDictionary Create(IRouteUpstream route) { return Create(route.UpstreamHeaderTemplates, route.RouteIsCaseSensitive); } public IDictionary Create(IHeaderDictionary upstreamHeaderTemplates, bool routeIsCaseSensitive) { var headers = upstreamHeaderTemplates.ToDictionary(h => h.Key, h => h.Value.ToString()); // TODO Review usage return Create(headers, routeIsCaseSensitive); } protected virtual IDictionary Create(IDictionary upstreamHeaderTemplates, bool routeIsCaseSensitive) { var result = new Dictionary(); foreach (var headerTemplate in upstreamHeaderTemplates) { var headerTemplateValue = headerTemplate.Value; var matches = RegexPlaceholders().Matches(headerTemplateValue); if (matches.Count > 0) { var placeholders = matches.Select(m => m.Groups[1].Value).ToArray(); for (int i = 0; i < placeholders.Length; i++) { var placeholder = placeholders[i]; var placeholderName = placeholder[8..^1]; // remove "{header:" and "}" headerTemplateValue = headerTemplateValue.Replace(placeholder, $"(?<{placeholderName}>.+)"); } } var template = routeIsCaseSensitive ? $"^{headerTemplateValue}$" : $"^(?i){headerTemplateValue}$"; // ignore case result.Add(headerTemplate.Key, new(template, headerTemplate.Value)); } return result; } } ================================================ FILE: src/Ocelot/Configuration/Creator/UpstreamTemplatePatternCreator.cs ================================================ using Ocelot.Cache; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Values; namespace Ocelot.Configuration.Creator; public class UpstreamTemplatePatternCreator : IUpstreamTemplatePatternCreator { public const string RegExMatchZeroOrMoreOfEverything = ".*"; private const string RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash = "[^/]+"; private const string RegExMatchEndString = "$"; private const string RegExIgnoreCase = "(?i)"; private const string RegExForwardSlashOnly = "^/$"; private const string RegExForwardSlashAndOnePlaceHolder = "^/.*"; private readonly IOcelotCache _cache; public UpstreamTemplatePatternCreator(IOcelotCache cache) { _cache = cache; } public UpstreamPathTemplate Create(IRouteUpstream route) { var upstreamTemplate = route.UpstreamPathTemplate; var placeholders = new List(); for (var i = 0; i < upstreamTemplate.Length; i++) { if (IsPlaceHolder(upstreamTemplate, i)) { var postitionOfPlaceHolderClosingBracket = upstreamTemplate.IndexOf('}', i); var difference = postitionOfPlaceHolderClosingBracket - i + 1; var placeHolderName = upstreamTemplate.Substring(i, difference); placeholders.Add(placeHolderName); // Hack to handle /{url} case if (ForwardSlashAndOnePlaceHolder(upstreamTemplate, placeholders, postitionOfPlaceHolderClosingBracket)) { return CreateTemplate(RegExForwardSlashAndOnePlaceHolder, 0, false, route.UpstreamPathTemplate); } } } var containsQueryString = false; if (upstreamTemplate.Contains('?')) { containsQueryString = true; upstreamTemplate = upstreamTemplate.Replace( upstreamTemplate.Contains("/?") ? "/?" : "?", @"(/$|/\?|\?|$)"); } for (var i = 0; i < placeholders.Count; i++) { var indexOfPlaceholder = upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal); var indexOfNextForwardSlash = upstreamTemplate.IndexOf("/", indexOfPlaceholder, StringComparison.Ordinal); if (indexOfNextForwardSlash < indexOfPlaceholder || (containsQueryString && upstreamTemplate.IndexOf('?', StringComparison.Ordinal) < upstreamTemplate.IndexOf(placeholders[i], StringComparison.Ordinal))) { upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchZeroOrMoreOfEverything); } else { upstreamTemplate = upstreamTemplate.Replace(placeholders[i], RegExMatchOneOrMoreOfEverythingUntilNextForwardSlash); } } if (upstreamTemplate == "/") { return CreateTemplate(RegExForwardSlashOnly, route.Priority, containsQueryString, route.UpstreamPathTemplate); } var index = upstreamTemplate.LastIndexOf('/'); // index of last forward slash if (index < (upstreamTemplate.Length - 1) && upstreamTemplate[index + 1] == '.') { upstreamTemplate = upstreamTemplate[..index] + "(?:|/" + upstreamTemplate[++index..] + ")"; } if (upstreamTemplate.EndsWith("/")) { upstreamTemplate = upstreamTemplate.Remove(upstreamTemplate.Length - 1, 1) + "(/|)"; } var template = route.RouteIsCaseSensitive ? $"^{upstreamTemplate}{RegExMatchEndString}" : $"^{RegExIgnoreCase}{upstreamTemplate}{RegExMatchEndString}"; return CreateTemplate(template, route.Priority, containsQueryString, route.UpstreamPathTemplate); } /// Time-to-live for caching to initialize the property. /// A constant structure, default absolute value is 1 minute. public static TimeSpan RegexCachingTTL { get; set; } = TimeSpan.FromMinutes(1.0D); protected Regex GetRegex(string key) { if (string.IsNullOrEmpty(key)) { return null; } if (!_cache.TryGetValue(key, nameof(UpstreamPathTemplate), out var rgx)) { rgx = RegexGlobal.New(key, RegexOptions.Singleline); _cache.Add(key, rgx, nameof(UpstreamPathTemplate), RegexCachingTTL); } return rgx; } protected UpstreamPathTemplate CreateTemplate(string template, int priority, bool containsQueryString, string originalValue) => new(template, priority, containsQueryString, originalValue) { Pattern = GetRegex(template), }; private static bool ForwardSlashAndOnePlaceHolder(string upstreamTemplate, List placeholders, int postitionOfPlaceHolderClosingBracket) => upstreamTemplate.Substring(0, 2) == "/{" && placeholders.Count == 1 && upstreamTemplate.Length == postitionOfPlaceHolderClosingBracket + 1; private static bool IsPlaceHolder(string upstreamTemplate, int i) => upstreamTemplate[i] == '{'; } ================================================ FILE: src/Ocelot/Configuration/Creator/VersionPolicies.cs ================================================ namespace Ocelot.Configuration.Creator; /// /// Constants for conversions in concrete classes for the interface. /// public class VersionPolicies { public const string RequestVersionExact = nameof(RequestVersionExact); public const string RequestVersionOrLower = nameof(RequestVersionOrLower); public const string RequestVersionOrHigher = nameof(RequestVersionOrHigher); } ================================================ FILE: src/Ocelot/Configuration/DownstreamHostAndPort.cs ================================================ namespace Ocelot.Configuration; public class DownstreamHostAndPort { public DownstreamHostAndPort(string host, int port) { Host = host; Port = port; } public string Host { get; } public int Port { get; } } ================================================ FILE: src/Ocelot/Configuration/DownstreamRoute.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Infrastructure.Extensions; using Ocelot.Values; namespace Ocelot.Configuration; public class DownstreamRoute { public DownstreamRoute( string key, UpstreamPathTemplate upstreamPathTemplate, List upstreamHeadersFindAndReplace, List downstreamHeadersFindAndReplace, List downstreamAddresses, string serviceName, string serviceNamespace, HttpHandlerOptions httpHandlerOptions, QoSOptions qosOptions, string downstreamScheme, string requestIdKey, CacheOptions cacheOptions, LoadBalancerOptions loadBalancerOptions, RateLimitOptions rateLimitOptions, Dictionary routeClaimsRequirement, List claimsToQueries, List claimsToHeaders, List claimsToClaims, List claimsToPath, AuthenticationOptions authenticationOptions, DownstreamPathTemplate downstreamPathTemplate, string loadBalancerKey, List delegatingHandlers, List addHeadersToDownstream, List addHeadersToUpstream, bool dangerousAcceptAnyServerCertificateValidator, SecurityOptions securityOptions, string downstreamHttpMethod, Version downstreamHttpVersion, HttpVersionPolicy downstreamHttpVersionPolicy, Dictionary upstreamHeaders, MetadataOptions metadataOptions, int? timeout) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; DelegatingHandlers = delegatingHandlers; Key = key; UpstreamPathTemplate = upstreamPathTemplate; UpstreamHeadersFindAndReplace = upstreamHeadersFindAndReplace ?? new List(); DownstreamHeadersFindAndReplace = downstreamHeadersFindAndReplace ?? new List(); DownstreamAddresses = downstreamAddresses ?? new List(); ServiceName = serviceName; ServiceNamespace = serviceNamespace; HttpHandlerOptions = httpHandlerOptions; QosOptions = qosOptions; DownstreamScheme = downstreamScheme; RequestIdKey = requestIdKey; CacheOptions = cacheOptions; LoadBalancerOptions = loadBalancerOptions; RateLimitOptions = rateLimitOptions; RouteClaimsRequirement = routeClaimsRequirement; ClaimsToQueries = claimsToQueries ?? new List(); ClaimsToHeaders = claimsToHeaders ?? new List(); ClaimsToClaims = claimsToClaims ?? new List(); ClaimsToPath = claimsToPath ?? new List(); AuthenticationOptions = authenticationOptions; DownstreamPathTemplate = downstreamPathTemplate; LoadBalancerKey = loadBalancerKey; AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); MetadataOptions = metadataOptions; Timeout = timeout; } public string Key { get; } public UpstreamPathTemplate UpstreamPathTemplate { get; } public List UpstreamHeadersFindAndReplace { get; } public List DownstreamHeadersFindAndReplace { get; } public List DownstreamAddresses { get; } public string ServiceName { get; } public string ServiceNamespace { get; } public HttpHandlerOptions HttpHandlerOptions { get; } public QoSOptions QosOptions { get; } public string DownstreamScheme { get; } public string RequestIdKey { get; } public CacheOptions CacheOptions { get; } public LoadBalancerOptions LoadBalancerOptions { get; } public RateLimitOptions RateLimitOptions { get; } public Dictionary RouteClaimsRequirement { get; } public List ClaimsToQueries { get; } public List ClaimsToHeaders { get; } public List ClaimsToClaims { get; } public List ClaimsToPath { get; } public bool IsAuthenticated => AuthenticationOptions is not null && !AuthenticationOptions.AllowAnonymous && AuthenticationOptions.HasScheme; public bool IsAuthorized => RouteClaimsRequirement?.Count > 0; public AuthenticationOptions AuthenticationOptions { get; } public DownstreamPathTemplate DownstreamPathTemplate { get; } public string LoadBalancerKey { get; } public List DelegatingHandlers { get; } public List AddHeadersToDownstream { get; } public List AddHeadersToUpstream { get; } public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } public Version DownstreamHttpVersion { get; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// An enum value being mapped from a constant. /// /// Related to the property. /// /// HttpVersionPolicy Enum /// HttpVersion Class /// HttpRequestMessage.VersionPolicy Property /// /// public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } public MetadataOptions MetadataOptions { get; } /// The timeout duration for the downstream request in seconds. /// A (T is ) value, in seconds. public int? Timeout { get; } public const int LowTimeout = 3; // 3 seconds public const int DefTimeout = 90; // 90 seconds /// Gets or sets the default timeout in seconds for all routes, applicable at both the route-level and globally. /// The setter includes a constraint that ensures the assigned value is greater than or equal to (3 seconds). /// By default, initialized to (90 seconds). /// An value in seconds. public static int DefaultTimeoutSeconds { get => defaultTimeoutSeconds; set => defaultTimeoutSeconds = value >= LowTimeout ? value : DefTimeout; } private static int defaultTimeoutSeconds = DefTimeout; public string Name() => Name(false); /// Gets the route name depending on whether the service discovery mode is enabled or disabled. /// A object with the name. public string Name(bool escapePath) { var path = !string.IsNullOrEmpty(UpstreamPathTemplate?.OriginalValue) ? UpstreamPathTemplate.OriginalValue : !string.IsNullOrEmpty(DownstreamPathTemplate.Value) // can't be null because it is created by DownstreamRouteBuilder ? DownstreamPathTemplate.ToString() : "?"; if (escapePath) { path = path.Replace("{", "{{").Replace("}", "}}"); } return UseServiceDiscovery || !string.IsNullOrEmpty(ServiceName) ? string.Join(':', ServiceNamespace, ServiceName, path) : path; } public override string ToString() => LoadBalancerKey; public bool UseServiceDiscovery => !ServiceName.IsEmpty(); } ================================================ FILE: src/Ocelot/Configuration/File/AggregateRouteConfig.cs ================================================ namespace Ocelot.Configuration.File; public class AggregateRouteConfig { public string RouteKey { get; set; } public string Parameter { get; set; } public string JsonPath { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileAggregateRoute.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.Configuration.File; public class FileAggregateRoute : IRouteUpstream, IRouteGroup { public string Aggregator { get; set; } public int Priority { get; set; } = 1; public bool RouteIsCaseSensitive { get; set; } public HashSet RouteKeys { get; set; } public List RouteKeysConfig { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } public string UpstreamHost { get; set; } public HashSet UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } public FileAggregateRoute() { Aggregator = default; Priority = 1; RouteIsCaseSensitive = default; RouteKeys = new(); RouteKeysConfig = new(); UpstreamHeaderTemplates = new Dictionary(); UpstreamHost = default; UpstreamHttpMethod = [ HttpMethods.Get ]; // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :) UpstreamPathTemplate = default; } } ================================================ FILE: src/Ocelot/Configuration/File/FileAuthenticationOptions.cs ================================================ using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.File; public class FileAuthenticationOptions { public FileAuthenticationOptions() { } public FileAuthenticationOptions(string authScheme) : this() => AuthenticationProviderKeys = [authScheme]; public FileAuthenticationOptions(FileAuthenticationOptions options) { ArgumentNullException.ThrowIfNull(options); AllowAnonymous = options.AllowAnonymous; AllowedScopes = options.AllowedScopes is null ? null : new(options.AllowedScopes); AuthenticationProviderKey = options.AuthenticationProviderKey; AuthenticationProviderKeys = new string[options.AuthenticationProviderKeys.Length]; Array.Copy(options.AuthenticationProviderKeys, AuthenticationProviderKeys, options.AuthenticationProviderKeys.Length); } public List AllowedScopes { get; set; } /// Allows anonymous authentication for route when global authentication options are used. /// if it is allowed; otherwise, . public bool? AllowAnonymous { get; set; } [Obsolete("Use AuthenticationProviderKeys instead of AuthenticationProviderKey! Note that AuthenticationProviderKey will be removed in version 25.0!")] public string AuthenticationProviderKey { get; set; } public string[] AuthenticationProviderKeys { get; set; } /// Checks whether authentication schemes are specified (not empty, exist). /// if an authentication scheme is defined; otherwise, . public bool HasScheme => AuthenticationProviderKey.IsNotEmpty() || AuthenticationProviderKeys?.Any(StringExtensions.IsNotEmpty) == true; public bool HasScope => AllowedScopes?.Exists(StringExtensions.IsNotEmpty) == true; public override string ToString() => new StringBuilder() .Append($"{nameof(AllowAnonymous)}:{AllowAnonymous ?? false},") .Append($"{nameof(AllowedScopes)}:[{AllowedScopes.NotNull().Select(x => $"'{x}'").Csv()}],") .Append($"{nameof(AuthenticationProviderKey)}:'{AuthenticationProviderKey}',") .Append($"{nameof(AuthenticationProviderKeys)}:[{AuthenticationProviderKeys.NotNull().Select(x => $"'{x}'").Csv()}]") .ToString(); } ================================================ FILE: src/Ocelot/Configuration/File/FileCacheOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileCacheOptions { public FileCacheOptions() { } public FileCacheOptions(int ttl) => TtlSeconds = ttl; public FileCacheOptions(FileCacheOptions from) { Region = from.Region; TtlSeconds = from.TtlSeconds; Header = from.Header; EnableContentHashing = from.EnableContentHashing; } /// Using where T is to have as default value and allowing global configuration usage. /// If then use global configuration with 0 by default. /// The time to live seconds, with 0 by default. public int? TtlSeconds { get; set; } public string Region { get; set; } public string Header { get; set; } /// Using where T is to have as default value and allowing global configuration usage. /// If then use global configuration with by default. /// if content hashing is enabled; otherwise, . public bool? EnableContentHashing { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileConfiguration.cs ================================================ namespace Ocelot.Configuration.File; public class FileConfiguration { public FileConfiguration() { Routes = new(); DynamicRoutes = new(); Aggregates = new(); GlobalConfiguration = new(); } public List Routes { get; set; } public List DynamicRoutes { get; set; } // Seperate field for aggregates because this let's you re-use Routes in multiple Aggregates public List Aggregates { get; set; } public FileGlobalConfiguration GlobalConfiguration { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileDynamicRoute.cs ================================================ namespace Ocelot.Configuration.File; /// /// Represents the JSON structure of a dynamic route in dynamic routing mode using service discovery. /// public class FileDynamicRoute : FileRouteBase, IRouteGrouping, IRouteRateLimiting { [Obsolete("Use RateLimitOptions instead of RateLimitRule! Note that RateLimitRule will be removed in version 25.0!")] public FileRateLimitByHeaderRule RateLimitRule { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalAuthenticationOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalAuthenticationOptions : FileAuthenticationOptions, IRouteGroup { public FileGlobalAuthenticationOptions() : base() { } public FileGlobalAuthenticationOptions(string authScheme) : base(authScheme) { } public FileGlobalAuthenticationOptions(FileAuthenticationOptions from) : base(from) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalCacheOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalCacheOptions : FileCacheOptions, IRouteGroup { public FileGlobalCacheOptions() : base() { } public FileGlobalCacheOptions(FileCacheOptions from) : base(from) { } public FileGlobalCacheOptions(int ttl) : base(ttl) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalConfiguration.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalConfiguration { public FileGlobalConfiguration() { AuthenticationOptions = new(); BaseUrl = default; CacheOptions = default; DownstreamHeaderTransform = new Dictionary(); DownstreamHttpVersion = default; DownstreamHttpVersionPolicy = default; DownstreamScheme = default; HttpHandlerOptions = new(); LoadBalancerOptions = default; Metadata = default; MetadataOptions = new(); QoSOptions = default; RateLimitOptions = default; RequestIdKey = default; SecurityOptions = new(); ServiceDiscoveryProvider = new(); Timeout = null; UpstreamHeaderTransform = new Dictionary(); } public FileGlobalAuthenticationOptions AuthenticationOptions { get; set; } public string BaseUrl { get; set; } public FileGlobalCacheOptions CacheOptions { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } public string DownstreamHttpVersion { get; set; } public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamScheme { get; set; } public FileGlobalHttpHandlerOptions HttpHandlerOptions { get; set; } public FileGlobalLoadBalancerOptions LoadBalancerOptions { get; set; } public IDictionary Metadata { get; set; } public FileMetadataOptions MetadataOptions { get; set; } public FileGlobalQoSOptions QoSOptions { get; set; } public FileGlobalRateLimitByHeaderRule RateLimitOptions { get; set; } public string RequestIdKey { get; set; } public FileSecurityOptions SecurityOptions { get; set; } public FileServiceDiscoveryProvider ServiceDiscoveryProvider { get; set; } /// Explicit timeout value which overrides default . /// Notes: /// /// is the consumer of this property. /// implicitly overrides this property if not defined (null). /// explicitly overrides this property if QoS is enabled. /// /// /// A (T is ) value, in seconds. public int? Timeout { get; set; } public IDictionary UpstreamHeaderTransform { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalHttpHandlerOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalHttpHandlerOptions : FileHttpHandlerOptions, IRouteGroup { public FileGlobalHttpHandlerOptions() : base() { } public FileGlobalHttpHandlerOptions(FileHttpHandlerOptions from) : base(from) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalLoadBalancerOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalLoadBalancerOptions : FileLoadBalancerOptions, IRouteGroup { public FileGlobalLoadBalancerOptions() : base() { } public FileGlobalLoadBalancerOptions(string type) : base(type) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalQoSOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalQoSOptions : FileQoSOptions, IRouteGroup { public FileGlobalQoSOptions() : base() { } public FileGlobalQoSOptions(FileQoSOptions from) : base(from) { } public FileGlobalQoSOptions(QoSOptions from) : base(from) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimit.cs ================================================ namespace Ocelot.Configuration.File; public sealed class FileGlobalRateLimit : FileGlobalRateLimitByMethodRule, // TODO This is temporarily solution to inherit from RL by Header feature model, an extraction of props is required IRouteGroup { // TODO Potentially, it should be 'Policy Name', or something that conveys the meaning of 'Rule Name' public string Name { get; init; } public string Pattern { get; init; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimitByAspNetRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalRateLimitByAspNetRule : FileRateLimitByAspNetRule, IRouteGroup { /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimitByHeaderRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalRateLimitByHeaderRule : FileRateLimitByHeaderRule, IRouteGroup { public FileGlobalRateLimitByHeaderRule() : base() { } public FileGlobalRateLimitByHeaderRule(FileRateLimitByHeaderRule from) : base(from) { } /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimitByIpRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalRateLimitByIpRule : FileRateLimitByIpRule, IRouteGroup { /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimitByMethodRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalRateLimitByMethodRule : FileRateLimitByMethodRule, IRouteGroup { /// Gets or sets the keys used to group routes, based on the already defined property. /// If not empty, these options are applied specifically to the route with those keys; otherwise, they are applied to all routes. /// A (where T is ) collection of keys that determine which routes the options should be applied to. public HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileGlobalRateLimiting.cs ================================================ namespace Ocelot.Configuration.File; public class FileGlobalRateLimiting : FileRateLimitRule { public FileGlobalRateLimitByHeaderRule[] ByHeader { get; set; } public /*FileGlobalRateLimitByMethodRule[]*/ FileGlobalRateLimit[] ByMethod { get; set; } // a prototype solution must be designed. Methods -> GET, POST, PUT etc. public FileGlobalRateLimitByIpRule[] ByIP { get; set; } // a prototype solution must be designed. Based on RemoteIpAddress public FileGlobalRateLimitByAspNetRule[] ByAspNet { get; set; } // a prototype solution must be designed public IDictionary Metadata { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileHostAndPort.cs ================================================ namespace Ocelot.Configuration.File; public class FileHostAndPort { public FileHostAndPort() { } public FileHostAndPort(FileHostAndPort from) { Host = from.Host; Port = from.Port; } public FileHostAndPort(string host, int port) { Host = host; Port = port; } public string Host { get; set; } public int Port { get; set; } public override string ToString() => $"{Host}:{Port}"; } ================================================ FILE: src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileHttpHandlerOptions { public FileHttpHandlerOptions() { } public FileHttpHandlerOptions(FileHttpHandlerOptions from) { AllowAutoRedirect = from.AllowAutoRedirect; MaxConnectionsPerServer = from.MaxConnectionsPerServer; PooledConnectionLifetimeSeconds = from.PooledConnectionLifetimeSeconds; UseCookieContainer = from.UseCookieContainer; UseProxy = from.UseProxy; UseTracing = from.UseTracing; } public bool? AllowAutoRedirect { get; set; } public int? MaxConnectionsPerServer { get; set; } public int? PooledConnectionLifetimeSeconds { get; set; } public bool? UseCookieContainer { get; set; } public bool? UseProxy { get; set; } public bool? UseTracing { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileLoadBalancerOptions { public FileLoadBalancerOptions() { } public FileLoadBalancerOptions(string type) : this() { Type = type; } public FileLoadBalancerOptions(FileLoadBalancerOptions from) { Expiry = from.Expiry; Key = from.Key; Type = from.Type; } public int? Expiry { get; set; } public string Key { get; set; } public string Type { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileMetadataOptions.cs ================================================ using System.Globalization; namespace Ocelot.Configuration.File; public class FileMetadataOptions { public FileMetadataOptions() { CurrentCulture = CultureInfo.CurrentCulture.Name; NumberStyle = Enum.GetName(NumberStyles.Any); Separators = new[] { "," }; StringSplitOption = Enum.GetName(StringSplitOptions.None); TrimChars = new[] { ' ' }; } public FileMetadataOptions(FileMetadataOptions from) { CurrentCulture = from.CurrentCulture; NumberStyle = from.NumberStyle; Separators = from.Separators; StringSplitOption = from.StringSplitOption; TrimChars = from.TrimChars; } public string CurrentCulture { get; set; } public string NumberStyle { get; set; } public string[] Separators { get; set; } public string StringSplitOption { get; set; } public char[] TrimChars { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileQoSOptions.cs ================================================ namespace Ocelot.Configuration.File; /// /// File model for the "Quality of Service" feature options of the route. /// public class FileQoSOptions { /// Initializes a new instance of the class. public FileQoSOptions() { } public FileQoSOptions(FileQoSOptions from) { DurationOfBreak = from.DurationOfBreak; BreakDuration = from.BreakDuration; ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; MinimumThroughput = from.MinimumThroughput; FailureRatio = from.FailureRatio; SamplingDuration = from.SamplingDuration; TimeoutValue = from.TimeoutValue; Timeout = from.Timeout; } public FileQoSOptions(QoSOptions from) { DurationOfBreak = from.BreakDuration; BreakDuration = from.BreakDuration; ExceptionsAllowedBeforeBreaking = from.MinimumThroughput; MinimumThroughput = from.MinimumThroughput; FailureRatio = from.FailureRatio; SamplingDuration = from.SamplingDuration; TimeoutValue = from.Timeout; Timeout = from.Timeout; } [Obsolete("Use BreakDuration instead of DurationOfBreak! Note that DurationOfBreak will be removed in version 25.0!")] public int? DurationOfBreak { get; set; } public int? BreakDuration { get; set; } [Obsolete("Use MinimumThroughput instead of ExceptionsAllowedBeforeBreaking! Note that ExceptionsAllowedBeforeBreaking will be removed in version 25.0!")] public int? ExceptionsAllowedBeforeBreaking { get; set; } public int? MinimumThroughput { get; set; } public double? FailureRatio { get; set; } public int? SamplingDuration { get; set; } /// Explicit timeout value which overrides default one. /// Reused in, or ignored in favor of implicit default value: /// /// /// /// /// /// /// A (T is ) value in milliseconds. [Obsolete("Use Timeout instead of TimeoutValue! Note that TimeoutValue will be removed in version 25.0!")] public int? TimeoutValue { get; set; } public int? Timeout { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimitByAspNetRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileRateLimitByAspNetRule { /// Gets or sets the policy name of ASP.NET Core rate limiter. /// A representing the policy name. public string Policy { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimitByHeaderRule.cs ================================================ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Ocelot.Infrastructure.Extensions; using Ocelot.RateLimiting; namespace Ocelot.Configuration.File; public class FileRateLimitByHeaderRule : FileRateLimitRule { public FileRateLimitByHeaderRule() : base() { } public FileRateLimitByHeaderRule(FileRateLimitRule from) : base(from) { } public FileRateLimitByHeaderRule(FileRateLimitByHeaderRule from) : base(from) { ClientIdHeader = from.ClientIdHeader; ClientWhitelist = from.ClientWhitelist; DisableRateLimitHeaders = from.DisableRateLimitHeaders; HttpStatusCode = from.HttpStatusCode; QuotaExceededMessage = from.QuotaExceededMessage; RateLimitCounterPrefix = from.RateLimitCounterPrefix; } /// Gets or sets the HTTP header used to store the client identifier, which defaults to Oc-Client. /// A representing the name of the HTTP header. public string ClientIdHeader { get; set; } /// A list of approved clients aka whitelisted ones. /// An collection of allowed clients. public IList ClientWhitelist { get; set; } /// /// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled ( is ). /// /// Format: H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]. /// A object. public override string ToString() { if (EnableRateLimiting == false) { return string.Empty; } /* string baseString = base.ToString(); char hdrSign = DisableRateLimitHeaders.HasValue ? (DisableRateLimitHeaders.Value ? '-' : '+') : baseString[1]; if (DisableRateLimitHeaders.HasValue && baseString[1] != hdrSign) { Span span = stackalloc byte[Encoding.ASCII.GetByteCount(baseString)]; Encoding.ASCII.GetBytes(baseString, span); span[1] = (byte)hdrSign; // replace hdr sign baseString = Encoding.ASCII.GetString(span); }*/ var baseString = base.ToString(); if (DisableRateLimitHeaders is bool disabled && baseString[1] != (disabled ? '-' : '+')) { baseString = string.Create(baseString.Length, (baseString, disabled), static (dstSpan, state) => { state.baseString.AsSpan().CopyTo(dstSpan); dstSpan[1] = state.disabled ? '-' : '+'; }); } string clHdr = ClientIdHeader.IfEmpty(None); string clLst = ClientWhitelist is null ? None : '[' + string.Join(',', ClientWhitelist) + ']'; return $"{baseString}/HDR:{clHdr}/WL{clLst}"; } /// Disables or enables X-RateLimit-* and Retry-After headers. /// A value, where T is . [Obsolete("Use EnableHeaders instead of DisableRateLimitHeaders! Note that DisableRateLimitHeaders will be removed in version 25.0!")] public bool? DisableRateLimitHeaders { get; set; } /// Gets or sets the rejection status code returned during the Quota Exceeded period, aka the wait window, or the remainder of the fixed window following the moment of exceeding. /// Default value: 429 (Too Many Requests). /// A value, where T is . [Obsolete("Use StatusCode instead of HttpStatusCode! Note that HttpStatusCode will be removed in version 25.0!")] public int? HttpStatusCode { get; set; } /// /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message. /// If none specified the default will be: . /// /// A value that will be used as a formatter. [Obsolete("Use QuotaMessage instead of QuotaExceededMessage! Note that QuotaExceededMessage will be removed in version 25.0!")] public string QuotaExceededMessage { get; set; } /// Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the service. /// Notes: /// /// The consumer is the method. /// The property is relevant for distributed storage systems, such as services, to inform users about which objects are being cached for management purposes. /// By default, each Ocelot instance uses its own service without cross-instance synchronization. /// /// /// A object which value defaults to "Ocelot.RateLimiting", see the property. [Obsolete("Use KeyPrefix instead of RateLimitCounterPrefix! Note that RateLimitCounterPrefix will be removed in version 25.0!")] public string RateLimitCounterPrefix { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimitByIpRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileRateLimitByIpRule : FileRateLimitRule { /// A list of allowed client's IP addresses aka whitelisted ones. /// An collection of allowed IPs. public IList IPWhitelist { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimitByMethodRule.cs ================================================ namespace Ocelot.Configuration.File; public class FileRateLimitByMethodRule : FileRateLimitRule { public HashSet Methods { get; init; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimitRule.cs ================================================ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Ocelot.Infrastructure.Extensions; using Ocelot.RateLimiting; namespace Ocelot.Configuration.File; public class FileRateLimitRule { public FileRateLimitRule() { } public FileRateLimitRule(FileRateLimitRule from) { ArgumentNullException.ThrowIfNull(from); EnableRateLimiting = from.EnableRateLimiting; EnableHeaders = from.EnableHeaders; Limit = from.Limit; Period = from.Period; PeriodTimespan = from.PeriodTimespan; Wait = from.Wait; StatusCode = from.StatusCode; QuotaMessage = from.QuotaMessage; KeyPrefix = from.KeyPrefix; } /// Enables or disables rate limiting. If undefined, it implicitly defaults to (enabled). /// A value, where T is . public bool? EnableRateLimiting { get; set; } /// Enables or disables X-RateLimit-* and Retry-After headers. /// A value, where T is . public bool? EnableHeaders { get; set; } /// The maximum number of requests a client can make within a given time . /// A value, where T is . public long? Limit { get; set; } /// Rate limiting period (fixed window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d). /// Defaults: If no unit is specified, the default unit is 'ms'. /// A object. public string Period { get; set; } /// The time interval to wait before sending a new request, measured in seconds. /// A value, where T is . [Obsolete("Use Wait instead of PeriodTimespan! Note that PeriodTimespan will be removed in version 25.0!")] public double? PeriodTimespan { get; set; } /// Rate limiting wait window (no servicing window) can be expressed as milliseconds (1ms), as seconds (1s), minutes (1m), hours (1h), or days (1d). /// Defaults: If no unit is specified, the default unit is 'ms'. /// A object. public string Wait { get; set; } /// Gets or sets the rejection status code returned during the Quota Exceeded period, aka the window, or the remainder of the fixed window following the moment of exceeding. /// Default value: 429 (Too Many Requests). /// A value, where T is . public int? StatusCode { get; set; } /// /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message. /// If none specified the default will be: . /// /// A value that will be used as a formatter. public string QuotaMessage { get; set; } /// Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the service. /// Notes: /// /// The consumer is the method. /// The property is relevant for distributed storage systems, such as services, to inform users about which objects are being cached for management purposes. /// By default, each Ocelot instance uses its own service without cross-instance synchronization. /// /// /// A object which value defaults to "Ocelot.RateLimiting", see the property. public string KeyPrefix { get; set; } /// /// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled ( is ). /// /// Format: H{+,-}:{limit}:{period,-}:w{wait,-}. /// A object. public override string ToString() { if (EnableRateLimiting == false) { return string.Empty; } char hdrSign = EnableHeaders == false ? '-' : '+'; string waitWindow = PeriodTimespan.HasValue ? PeriodTimespan.Value.ToString("F3") + 's' : Wait.IfEmpty(None); return $"H{hdrSign}:{Limit}:{Period}:w{waitWindow}"; } public const string None = "-"; } ================================================ FILE: src/Ocelot/Configuration/File/FileRateLimiting.cs ================================================ namespace Ocelot.Configuration.File; public class FileRateLimiting : FileRateLimitRule { public FileRateLimitByHeaderRule ByHeader { get; set; } public /*FileRateLimitByMethodRule*/ FileGlobalRateLimit ByMethod { get; set; } // a prototype solution must be designed. Methods -> GET, POST, PUT etc. public FileRateLimitByIpRule ByIP { get; set; } // a prototype solution must be designed. Based on RemoteIpAddress public FileRateLimitByAspNetRule ByAspNet { get; set; } // a prototype solution must be designed public IDictionary Metadata { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRoute.cs ================================================ namespace Ocelot.Configuration.File; /// /// Represents the JSON structure of a standard static route (no service discovery). /// public class FileRoute : FileRouteBase, IRouteUpstream, IRouteGrouping, IRouteRateLimiting, ICloneable { public FileRoute() { AddClaimsToRequest = new Dictionary(); AddHeadersToRequest = new Dictionary(); AddQueriesToRequest = new Dictionary(); ChangeDownstreamPathTemplate = new Dictionary(); DelegatingHandlers = new List(); DownstreamHeaderTransform = new Dictionary(); DownstreamHostAndPorts = new List(); Priority = 1; // to be reviewed WTF? RouteClaimsRequirement = new Dictionary(); SecurityOptions = new FileSecurityOptions(); UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new(); } public FileRoute(FileRoute from) { DeepCopy(from, this); } public Dictionary AddClaimsToRequest { get; set; } public Dictionary AddHeadersToRequest { get; set; } public Dictionary AddQueriesToRequest { get; set; } public Dictionary ChangeDownstreamPathTemplate { get; set; } public bool DangerousAcceptAnyServerCertificateValidator { get; set; } public List DelegatingHandlers { get; set; } public IDictionary DownstreamHeaderTransform { get; set; } public List DownstreamHostAndPorts { get; set; } public string DownstreamHttpMethod { get; set; } public string DownstreamPathTemplate { get; set; } [Obsolete("Use CacheOptions instead of FileCacheOptions! Note that FileCacheOptions will be removed in version 25.0!")] public FileCacheOptions FileCacheOptions { get; set; } public int Priority { get; set; } public string RequestIdKey { get; set; } public Dictionary RouteClaimsRequirement { get; set; } public bool RouteIsCaseSensitive { get; set; } public FileSecurityOptions SecurityOptions { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } public IDictionary UpstreamHeaderTransform { get; set; } public string UpstreamHost { get; set; } public HashSet UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } /// /// Clones this object by making a deep copy. /// /// A deeply copied object. public object Clone() { var other = (FileRoute)MemberwiseClone(); DeepCopy(this, other); return other; } public static void DeepCopy(FileRoute from, FileRoute to) { to.AddClaimsToRequest = new(from.AddClaimsToRequest); to.AddHeadersToRequest = new(from.AddHeadersToRequest); to.AddQueriesToRequest = new(from.AddQueriesToRequest); to.AuthenticationOptions = from.AuthenticationOptions is null ? null : new(from.AuthenticationOptions); to.ChangeDownstreamPathTemplate = new(from.ChangeDownstreamPathTemplate); to.DangerousAcceptAnyServerCertificateValidator = from.DangerousAcceptAnyServerCertificateValidator; to.DelegatingHandlers = new(from.DelegatingHandlers); to.DownstreamHeaderTransform = new Dictionary(from.DownstreamHeaderTransform); to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList(); to.DownstreamHttpMethod = from.DownstreamHttpMethod; to.DownstreamHttpVersion = from.DownstreamHttpVersion; to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; to.DownstreamPathTemplate = from.DownstreamPathTemplate; to.DownstreamScheme = from.DownstreamScheme; to.CacheOptions = new(from.CacheOptions); to.FileCacheOptions = new(from.FileCacheOptions); to.HttpHandlerOptions = new(from.HttpHandlerOptions); to.Key = from.Key; to.LoadBalancerOptions = new(from.LoadBalancerOptions); to.Metadata = new Dictionary(from.Metadata); to.Priority = from.Priority; to.QoSOptions = new(from.QoSOptions); to.RateLimiting = from.RateLimiting; // new(from.RateLimiting) to.RateLimitOptions = new(from.RateLimitOptions); to.RequestIdKey = from.RequestIdKey; to.RouteClaimsRequirement = new(from.RouteClaimsRequirement); to.RouteIsCaseSensitive = from.RouteIsCaseSensitive; to.SecurityOptions = new(from.SecurityOptions); to.ServiceName = from.ServiceName; to.ServiceNamespace = from.ServiceNamespace; to.Timeout = from.Timeout; to.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); to.UpstreamHeaderTransform = new Dictionary(from.UpstreamHeaderTransform); to.UpstreamHost = from.UpstreamHost; to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); to.UpstreamPathTemplate = from.UpstreamPathTemplate; } public override string ToString() { if (!string.IsNullOrWhiteSpace(Key)) { return Key; } var path = !string.IsNullOrEmpty(UpstreamPathTemplate) ? UpstreamPathTemplate : !string.IsNullOrEmpty(DownstreamPathTemplate) ? DownstreamPathTemplate : "?"; return !string.IsNullOrWhiteSpace(ServiceName) ? string.Join(':', ServiceNamespace, ServiceName, path) : path; } } ================================================ FILE: src/Ocelot/Configuration/File/FileRouteBase.cs ================================================ using Ocelot.Configuration.Creator; using System.Text.Json.Serialization; using NewtonsoftJsonIgnore = Newtonsoft.Json.JsonIgnoreAttribute; namespace Ocelot.Configuration.File; /// /// Defines common aggregation for dynamic and static routes. /// public abstract class FileRouteBase : IRouteGrouping { public FileAuthenticationOptions AuthenticationOptions { get; set; } public FileCacheOptions CacheOptions { get; set; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// A value of defined constants. /// /// Related to the property. /// /// HttpVersionPolicy Enum /// HttpVersion Class /// HttpRequestMessage.VersionPolicy Property /// /// public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamHttpVersion { get; set; } public string DownstreamScheme { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } // IRouteGrouping public FileLoadBalancerOptions LoadBalancerOptions { get; set; } public IDictionary Metadata { get; set; } public FileQoSOptions QoSOptions { get; set; } public FileRateLimitByHeaderRule RateLimitOptions { get; set; } // IRouteRateLimiting [NewtonsoftJsonIgnore, JsonIgnore] // publish the schema in version 25.1! public FileRateLimiting RateLimiting { get; set; } public string ServiceName { get; set; } public string ServiceNamespace { get; set; } /// Explicit timeout value which overrides default . /// Notes: /// /// is the consumer of this property. /// implicitly overrides this property if not defined (null). /// explicitly overrides this property if QoS is enabled. /// /// /// A (T is ) value, in seconds. public int? Timeout { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/FileSecurityOptions.cs ================================================ namespace Ocelot.Configuration.File; public class FileSecurityOptions { public FileSecurityOptions() { IPAllowedList = new(); IPBlockedList = new(); ExcludeAllowedFromBlocked = false; } public FileSecurityOptions(FileSecurityOptions from) { IPAllowedList = new(from.IPAllowedList); IPBlockedList = new(from.IPBlockedList); ExcludeAllowedFromBlocked = from.ExcludeAllowedFromBlocked; } public FileSecurityOptions(string allowedIPs = null, string blockedIPs = null, bool? excludeAllowedFromBlocked = null) : this() { if (!string.IsNullOrEmpty(allowedIPs)) { IPAllowedList.Add(allowedIPs); } if (!string.IsNullOrEmpty(blockedIPs)) { IPBlockedList.Add(blockedIPs); } ExcludeAllowedFromBlocked = excludeAllowedFromBlocked ?? false; } public FileSecurityOptions(IEnumerable allowedIPs = null, IEnumerable blockedIPs = null, bool? excludeAllowedFromBlocked = null) : this() { IPAllowedList.AddRange(allowedIPs ?? Enumerable.Empty()); IPBlockedList.AddRange(blockedIPs ?? Enumerable.Empty()); ExcludeAllowedFromBlocked = excludeAllowedFromBlocked ?? false; } public List IPAllowedList { get; set; } public List IPBlockedList { get; set; } /// Provides the ability to specify a wide range of blocked IP addresses and allow a subrange of IP addresses. /// A value, defaults to . public bool ExcludeAllowedFromBlocked { get; set; } public bool IsEmpty() => IPAllowedList.Count == 0 && IPBlockedList.Count == 0; } ================================================ FILE: src/Ocelot/Configuration/File/FileServiceDiscoveryProvider.cs ================================================ namespace Ocelot.Configuration.File; public class FileServiceDiscoveryProvider { public string Scheme { get; set; } public string Host { get; set; } public int Port { get; set; } public string Type { get; set; } public string Token { get; set; } public string ConfigurationKey { get; set; } public int PollingInterval { get; set; } public string Namespace { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/IRouteGroup.cs ================================================ namespace Ocelot.Configuration.File; /// /// Provides support for creating a group of routes, instances of . /// public interface IRouteGroup { /// The group's list of route keys (the property). /// A collection, where T is a , containing key strings. HashSet RouteKeys { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/IRouteGrouping.cs ================================================ namespace Ocelot.Configuration.File; /// /// Allows to add this route to a group of routes as an object. /// public interface IRouteGrouping { /// The key for this route is used to group it as part of the collection. /// A object, containing key. string Key { get; set; } } ================================================ FILE: src/Ocelot/Configuration/File/IRouteRateLimiting.cs ================================================ namespace Ocelot.Configuration.File; public interface IRouteRateLimiting : IRouteGrouping { FileRateLimitByHeaderRule RateLimitOptions { get; } } ================================================ FILE: src/Ocelot/Configuration/File/IRouteUpstream.cs ================================================ namespace Ocelot.Configuration.File; public interface IRouteUpstream { IDictionary UpstreamHeaderTemplates { get; } string UpstreamPathTemplate { get; } HashSet UpstreamHttpMethod { get; } bool RouteIsCaseSensitive { get; } int Priority { get; } } ================================================ FILE: src/Ocelot/Configuration/HeaderFindAndReplace.cs ================================================ namespace Ocelot.Configuration; public class HeaderFindAndReplace { public const char Comma = ','; public HeaderFindAndReplace(HeaderFindAndReplace from) { ArgumentNullException.ThrowIfNull(from, nameof(from)); Index = from.Index; Key = from.Key; Find = from.Find; Replace = from.Replace; } public HeaderFindAndReplace(KeyValuePair from) { Index = 0; Key = from.Key; if (!string.IsNullOrWhiteSpace(from.Value) && from.Value.Contains(Comma)) { string[] parsed = from.Value.Split(Comma); Find = parsed[0].Trim(); Replace = parsed[1].Trim(); } else { Find = from.Value?.Trim() ?? string.Empty; Replace = string.Empty; } } public HeaderFindAndReplace(string key, string find, string replace, int index) { Key = key; Find = find; Replace = replace; Index = index; } public string Key { get; } public string Find { get; } public string Replace { get; } // only index 0 for now.. public int Index { get; } public override string ToString() => $"{nameof(HeaderFindAndReplace)}[{Key} at {Index}: {Find} -> {Replace}]"; } ================================================ FILE: src/Ocelot/Configuration/HttpHandlerOptions.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration; /// /// Describes configuration parameters for http handler, that is created to handle a request to service. /// public class HttpHandlerOptions //: SocketsHttpHandler // TODO Think about using inheritance or composition design since we initialize the SocketsHttpHandler instance with the options { public const int DefaultPooledConnectionLifetimeSeconds = 120; public HttpHandlerOptions() { MaxConnectionsPerServer = int.MaxValue; PooledConnectionLifeTime = TimeSpan.FromSeconds(DefaultPooledConnectionLifetimeSeconds); } public HttpHandlerOptions(FileHttpHandlerOptions from) { AllowAutoRedirect = from.AllowAutoRedirect ?? false; MaxConnectionsPerServer = from.MaxConnectionsPerServer.HasValue && from.MaxConnectionsPerServer.Value > 0 ? from.MaxConnectionsPerServer.Value : int.MaxValue; PooledConnectionLifeTime = TimeSpan.FromSeconds(from.PooledConnectionLifetimeSeconds ?? DefaultPooledConnectionLifetimeSeconds); UseCookieContainer = from.UseCookieContainer ?? false; UseProxy = from.UseProxy ?? false; UseTracing = from.UseTracing ?? false; } public HttpHandlerOptions(FileHttpHandlerOptions from, bool useTracing) : this(from) { UseTracing = useTracing && (from.UseTracing ?? false); } /// /// Specify if auto redirect is enabled. /// /// AllowAutoRedirect. public bool AllowAutoRedirect { get; init; } /// /// Specify is handler has to use a cookie container. /// /// UseCookieContainer. public bool UseCookieContainer { get; init; } /// /// Specify is handler has to use a opentracing. /// /// UseTracing. public bool UseTracing { get; init; } /// /// Specify if handler has to use a proxy. /// /// UseProxy. public bool UseProxy { get; init; } /// /// Specify the maximum of concurrent connection to a network endpoint. /// /// MaxConnectionsPerServer. public int MaxConnectionsPerServer { get; init; } /// /// Specify the maximum of time a connection can be pooled. /// /// PooledConnectionLifeTime. public TimeSpan PooledConnectionLifeTime { get; init; } } ================================================ FILE: src/Ocelot/Configuration/IInternalConfiguration.cs ================================================ namespace Ocelot.Configuration; public interface IInternalConfiguration { string AdministrationPath { get; } AuthenticationOptions AuthenticationOptions { get; } CacheOptions CacheOptions { get; } Version DownstreamHttpVersion { get; } HttpVersionPolicy DownstreamHttpVersionPolicy { get; } string DownstreamScheme { get; } HttpHandlerOptions HttpHandlerOptions { get; } LoadBalancerOptions LoadBalancerOptions { get; } MetadataOptions MetadataOptions { get; } QoSOptions QoSOptions { get; } RateLimitOptions RateLimitOptions { get; } string RequestId { get; } Route[] Routes { get; } ServiceProviderConfiguration ServiceProviderConfiguration { get; } int? Timeout { get; } } ================================================ FILE: src/Ocelot/Configuration/InternalConfiguration.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration; public class InternalConfiguration : IInternalConfiguration { public InternalConfiguration() => Routes = []; public InternalConfiguration(Route[] routes) => Routes = routes ?? []; public string AdministrationPath { get; init; } public AuthenticationOptions AuthenticationOptions { get; init; } public CacheOptions CacheOptions { get; set; } public Version DownstreamHttpVersion { get; init; } /// Global HTTP version policy. It is related to property. /// An enumeration value. public HttpVersionPolicy DownstreamHttpVersionPolicy { get; init; } public string DownstreamScheme { get; init; } public HttpHandlerOptions HttpHandlerOptions { get; init; } public LoadBalancerOptions LoadBalancerOptions { get; init; } public MetadataOptions MetadataOptions { get; init; } public QoSOptions QoSOptions { get; init; } public RateLimitOptions RateLimitOptions { get; init; } public string RequestId { get; init; } public Route[] Routes { get; init; } public ServiceProviderConfiguration ServiceProviderConfiguration { get; init; } public int? Timeout { get; init; } } ================================================ FILE: src/Ocelot/Configuration/LoadBalancerOptions.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer.Balancers; namespace Ocelot.Configuration; public class LoadBalancerOptions { public LoadBalancerOptions() { Type = nameof(NoLoadBalancer); } public LoadBalancerOptions(FileLoadBalancerOptions options) : this(options?.Type, options?.Key, options?.Expiry) { } public LoadBalancerOptions(string type, string key, int? expiryInMs) { Type = type.IfEmpty(nameof(NoLoadBalancer)); bool isStickySessions = nameof(CookieStickySessions).Equals(type, StringComparison.OrdinalIgnoreCase); Key = isStickySessions ? key.IfEmpty(CookieStickySessions.DefSessionCookieName) : key; ExpiryInMs = isStickySessions ? expiryInMs ?? CookieStickySessions.DefSessionExpiryMilliseconds : expiryInMs ?? 0; } public string Type { get; init; } public string Key { get; init; } public int ExpiryInMs { get; init; } } ================================================ FILE: src/Ocelot/Configuration/MetadataOptions.cs ================================================ using Ocelot.Configuration.File; using System.Globalization; namespace Ocelot.Configuration; public class MetadataOptions { public MetadataOptions() { CurrentCulture = CultureInfo.CurrentCulture; NumberStyle = NumberStyles.Any; Separators = new[] { "," }; StringSplitOption = StringSplitOptions.None; TrimChars = new[] { ' ' }; Metadata = new Dictionary(); } public MetadataOptions(MetadataOptions from) { CurrentCulture = from.CurrentCulture; NumberStyle = from.NumberStyle; Separators = from.Separators; StringSplitOption = from.StringSplitOption; TrimChars = from.TrimChars; Metadata = from.Metadata; } public MetadataOptions(FileMetadataOptions from) { CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture); NumberStyle = Enum.Parse(from.NumberStyle); Separators = from.Separators; StringSplitOption = Enum.Parse(from.StringSplitOption); TrimChars = from.TrimChars; Metadata = new Dictionary(); } public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption, NumberStyles numberStyle, CultureInfo currentCulture, IDictionary metadata) { CurrentCulture = currentCulture; NumberStyle = numberStyle; Separators = separators; StringSplitOption = stringSplitOption; TrimChars = trimChars; Metadata = metadata; } public CultureInfo CurrentCulture { get; } public NumberStyles NumberStyle { get; } public string[] Separators { get; } public StringSplitOptions StringSplitOption { get; } public char[] TrimChars { get; } public IDictionary Metadata { get; set; } } ================================================ FILE: src/Ocelot/Configuration/Parser/ClaimToThingConfigurationParser.cs ================================================ using Ocelot.Infrastructure; using Ocelot.Responses; namespace Ocelot.Configuration.Parser; /// /// Default implementation of the interface. /// public partial class ClaimToThingConfigurationParser : IClaimToThingConfigurationParser { private const char SplitToken = '>'; [GeneratedRegex("Claims\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex ClaimRegex(); [GeneratedRegex("value\\[.*\\]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex IndexRegex(); public Response Extract(string existingKey, string value) { try { var instructions = value.Split(SplitToken); if (instructions.Length <= 1) { return new ErrorResponse(new NoInstructionsError(SplitToken.ToString())); } var claimMatch = ClaimRegex().IsMatch(instructions[0]); if (!claimMatch) { return new ErrorResponse(new InstructionNotForClaimsError()); } var newKey = GetIndexValue(instructions[0]); var index = 0; var delimiter = string.Empty; if (instructions.Length > 2 && IndexRegex().IsMatch(instructions[1])) { index = int.Parse(GetIndexValue(instructions[1])); delimiter = instructions[2].Trim(); } return new OkResponse( new ClaimToThing(existingKey, newKey, delimiter, index)); } catch (Exception exception) { return new ErrorResponse(new ParsingConfigurationHeaderError(exception)); } } private static string GetIndexValue(string instruction) { var firstIndexer = instruction.IndexOf('[', StringComparison.Ordinal); var lastIndexer = instruction.IndexOf(']', StringComparison.Ordinal); var length = lastIndexer - firstIndexer; var claimKey = instruction.Substring(firstIndexer + 1, length - 1); return claimKey; } } ================================================ FILE: src/Ocelot/Configuration/Parser/IClaimToThingConfigurationParser.cs ================================================ using Ocelot.Responses; namespace Ocelot.Configuration.Parser; public interface IClaimToThingConfigurationParser { Response Extract(string existingKey, string value); } ================================================ FILE: src/Ocelot/Configuration/Parser/InstructionNotForClaimsError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Configuration.Parser; public class InstructionNotForClaimsError : Error { public InstructionNotForClaimsError() : base("instructions did not contain claims, at the moment we only support claims extraction", OcelotErrorCode.InstructionNotForClaimsError, 404) { } } ================================================ FILE: src/Ocelot/Configuration/Parser/NoInstructionsError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Configuration.Parser; public class NoInstructionsError : Error { public NoInstructionsError(string splitToken) : base($"There we no instructions splitting on {splitToken}", OcelotErrorCode.NoInstructionsError, 404) { } } ================================================ FILE: src/Ocelot/Configuration/Parser/ParsingConfigurationHeaderError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.Configuration.Parser; public class ParsingConfigurationHeaderError : Error { public ParsingConfigurationHeaderError(Exception exception) : base($"Parsing configuration exception is {exception.Message}", OcelotErrorCode.ParsingConfigurationHeaderError, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/Configuration/QoSOptions.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Configuration; public class QoSOptions { public QoSOptions() { } public QoSOptions(int? timeout) => Timeout = timeout; public QoSOptions(int? exceptions, int? breakMs) { BreakDuration = breakMs; MinimumThroughput = exceptions; } /// Initializes a new instance of the class. /// This is the copying constructor. /// The object to copy the properties from. public QoSOptions(QoSOptions from) { BreakDuration = from.BreakDuration; MinimumThroughput = from.MinimumThroughput; FailureRatio = from.FailureRatio; SamplingDuration = from.SamplingDuration; Timeout = from.Timeout; } /// Initializes a new instance of the class from a model. /// This is the converting constructor. /// The File-model to copy the properties from. public QoSOptions(FileQoSOptions from) { BreakDuration = from.DurationOfBreak ?? from.BreakDuration; MinimumThroughput = from.ExceptionsAllowedBeforeBreaking ?? from.MinimumThroughput; FailureRatio = from.FailureRatio; SamplingDuration = from.SamplingDuration; Timeout = from.TimeoutValue ?? from.Timeout; } /// Gets the duration, in milliseconds, that the circuit remains open before resetting. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class. /// A (T is ) value (milliseconds). public int? BreakDuration { get; init; } /// Gets the minimum number of failures required before the circuit is set to open. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class. /// A (T is ) value (exceptions number). public int? MinimumThroughput { get; init; } /// Gets or sets the failure-to-success ratio at which the circuit will break. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the CircuitBreakerStrategy class. /// A (T is ) value. public double? FailureRatio { get; init; } /// Gets or sets the milliseconds duration of the sampling over which is assessed. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the TimeoutStrategy class. /// A (T is ) value (milliseconds). public int? SamplingDuration { get; init; } /// Gets the timeout in milliseconds. /// Note: Read the appropriate documentation in the Ocelot.Provider.Polly project, which is the sole consumer of this property. See the TimeoutStrategy class. /// A (T is ) value (milliseconds). public int? Timeout { get; init; } public bool UseQos => (MinimumThroughput.HasValue && MinimumThroughput > 0) || (Timeout.HasValue && Timeout > 0); } ================================================ FILE: src/Ocelot/Configuration/RateLimitOptions.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; using Ocelot.RateLimiting; namespace Ocelot.Configuration; /// /// RateLimit Options. /// public class RateLimitOptions { public const string DefaultClientHeader = "Oc-Client"; public static readonly string DefaultCounterPrefix = typeof(RateLimiting.RateLimiting).Namespace; public const int DefaultStatus429 = StatusCodes.Status429TooManyRequests; public const string DefaultQuotaMessage = "API calls quota exceeded! Maximum admitted {0} per {1}."; public RateLimitOptions() { ClientIdHeader = DefaultClientHeader; ClientWhitelist = []; EnableHeaders = true; EnableRateLimiting = true; StatusCode = DefaultStatus429; QuotaMessage = DefaultQuotaMessage; KeyPrefix = DefaultCounterPrefix; Rule = RateLimitRule.Empty; } public RateLimitOptions(bool enableRateLimiting) : this() { EnableRateLimiting = enableRateLimiting; } public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IList clientWhitelist, bool enableHeaders, string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode) { ClientIdHeader = clientIdHeader.IfEmpty(DefaultClientHeader); ClientWhitelist = clientWhitelist ?? []; EnableHeaders = enableHeaders; EnableRateLimiting = enableRateLimiting; KeyPrefix = rateLimitCounterPrefix.IfEmpty(DefaultCounterPrefix); QuotaMessage = quotaExceededMessage.IfEmpty(DefaultQuotaMessage); Rule = rateLimitRule; StatusCode = httpStatusCode; } public RateLimitOptions(FileRateLimitByHeaderRule fromRule) { ArgumentNullException.ThrowIfNull(fromRule); ClientIdHeader = fromRule.ClientIdHeader.IfEmpty(DefaultClientHeader); ClientWhitelist = fromRule.ClientWhitelist ?? []; EnableHeaders = fromRule.DisableRateLimitHeaders.HasValue ? !fromRule.DisableRateLimitHeaders.Value : fromRule.EnableHeaders ?? true; EnableRateLimiting = fromRule.EnableRateLimiting ?? true; StatusCode = fromRule.HttpStatusCode ?? fromRule.StatusCode ?? DefaultStatus429; QuotaMessage = fromRule.QuotaExceededMessage.IfEmpty(fromRule.QuotaMessage.IfEmpty(DefaultQuotaMessage)); KeyPrefix = fromRule.RateLimitCounterPrefix.IfEmpty(fromRule.KeyPrefix.IfEmpty(DefaultCounterPrefix)); Rule = new( fromRule.Period.IfEmpty(RateLimitRule.DefaultPeriod), fromRule.PeriodTimespan.HasValue ? $"{fromRule.PeriodTimespan.Value}s" : fromRule.Wait, fromRule.Limit ?? RateLimitRule.ZeroLimit); } public RateLimitOptions(RateLimitOptions fromOptions) { ArgumentNullException.ThrowIfNull(fromOptions); ClientIdHeader = fromOptions.ClientIdHeader.IfEmpty(DefaultClientHeader); ClientWhitelist = fromOptions.ClientWhitelist ?? []; EnableHeaders = fromOptions.EnableHeaders; EnableRateLimiting = fromOptions.EnableRateLimiting; StatusCode = fromOptions.StatusCode; QuotaMessage = fromOptions.QuotaMessage.IfEmpty(DefaultQuotaMessage); KeyPrefix = fromOptions.KeyPrefix.IfEmpty(DefaultCounterPrefix); Rule = fromOptions.Rule ?? RateLimitRule.Empty; } /// Gets a Rate Limit rule. /// A object that represents the rule. public RateLimitRule Rule { get; init; } /// A list of approved clients aka whitelisted ones. /// An collection of allowed clients. public IList ClientWhitelist { get; init; } /// Gets or sets the HTTP header used to store the client identifier, which defaults to Oc-Client. /// A representing the name of the HTTP header. public string ClientIdHeader { get; init; } /// Gets or sets the rejection status code returned during the Quota Exceeded period, aka the window, or the remainder of the fixed window following the moment of exceeding. /// Default value: 429 (Too Many Requests). /// A value. public int StatusCode { get; init; } /// /// Gets or sets a value to be used as the formatter for the Quota Exceeded response message. /// If none specified the default will be: . /// /// A value that will be used as a formatter. public string QuotaMessage { get; init; } /// Gets or sets the counter prefix, used to compose the rate limiting counter caching key to be used by the service. /// Notes: /// /// The consumer is the method. /// The property is relevant for distributed storage systems, such as services, to inform users about which objects are being cached for management purposes. /// By default, each Ocelot instance uses its own service without cross-instance synchronization. /// /// /// A object which value defaults to "Ocelot.RateLimiting", see the property. public string KeyPrefix { get; init; } /// Enables or disables rate limiting. Defaults to (enabled). /// A value. public bool EnableRateLimiting { get; init; } /// Enables or disables X-RateLimit-* and Retry-After headers. /// A value. public bool EnableHeaders { get; init; } } ================================================ FILE: src/Ocelot/Configuration/RateLimitRule.cs ================================================ using Ocelot.Infrastructure.Extensions; using System.Globalization; namespace Ocelot.Configuration; public class RateLimitRule { public const string DefaultPeriod = "1s"; public const string ZeroWait = "0ms"; public const long ZeroLimit = 0L; public static RateLimitRule Empty = new(DefaultPeriod, ZeroWait, ZeroLimit); public RateLimitRule(string period, string wait, long limit) { Period = period.IfEmpty(DefaultPeriod); Wait = wait.IfEmpty(ZeroWait); Limit = Math.Abs(limit); } public override string ToString() => $"{Limit}/{Period}/w{Wait}"; /// /// Rate limiting durations can be set using units like 'ms' (milliseconds), 's' (seconds), 'm' (minutes), 'h' (hours), or 'd' (days). /// /// A object with the period (fixed window). public string Period { get; } /// A processed form of the property optimized for quick algorithm computations. /// A value. public TimeSpan PeriodSpan { get => _periodSpan ??= ParseTimespan(Period); } private TimeSpan? _periodSpan; /// /// Wait window after exceeding the rate limit, which has 'ms', 's', 'm', 'h', 'd' units. /// /// A object with the waiting window. public string Wait { get; } /// A processed form of the property optimized for quick algorithm computations. /// A value. public TimeSpan WaitSpan { get => _waitSpan ??= ParseTimespan(Wait); } private TimeSpan? _waitSpan; /// /// Maximum number of requests that a client can make in a defined period. /// /// A value with maximum number of requests. public long Limit { get; } /// /// Parses a timespan string, such as "1ms", "1s", "1m", "1h", "1d". /// /// Converts a string to milliseconds when the unit is missing or undefined, automatically applying the 'ms' unit. /// The string value with units: '1ms', '1s', '1m', '1h', '1d'. /// A value. /// If the value is not a number, or the unit of value cannot be determined. public static TimeSpan ParseTimespan(string timespan) { if (string.IsNullOrWhiteSpace(timespan)) { return TimeSpan.Zero; } if (!timespan.Any(char.IsDigit)) { // TODO: Make sense to have validation in src/Ocelot/Configuration/Validator/RouteFluentValidator throw new FormatException($"The '{timespan}' value doesn't include any digits, so it cannot be considered a number!"); } string val = timespan.Trim(); int pos = val.Length; while (--pos >= 0 && !char.IsDigit(val[pos]) && val[pos] != DecimalSeparator) { } string floating = val[..++pos], unit = val[pos..]; double value = Math.Abs(double.Parse(floating)); // negative values should be disallowed as they could cause everything to malfunction; TODO: Make sense to have validation in src/Ocelot/Configuration/Validator/RouteFluentValidator return unit.ToLower() switch { "d" => TimeSpan.FromDays(value), "h" => TimeSpan.FromHours(value), "m" => TimeSpan.FromMinutes(value), "s" => TimeSpan.FromSeconds(value), "ms" => TimeSpan.FromMilliseconds(value), "" => TimeSpan.FromMilliseconds(value), // an unknown unit defaults to milliseconds as the ms unit _ => throw new FormatException($"The '{timespan}' timespan cannot be converted to {nameof(TimeSpan)} due to an unknown '{unit}' unit!"), }; } private static readonly char DecimalSeparator = new NumberFormatInfo().NumberDecimalSeparator[0]; } ================================================ FILE: src/Ocelot/Configuration/Repository/ConsulFileConfigurationPollerOption.cs ================================================ namespace Ocelot.Configuration.Repository; public class ConsulFileConfigurationPollerOption : IFileConfigurationPollerOptions { private readonly IInternalConfigurationRepository _internalConfigRepo; private readonly IFileConfigurationRepository _fileConfigurationRepository; public ConsulFileConfigurationPollerOption(IInternalConfigurationRepository internalConfigurationRepository, IFileConfigurationRepository fileConfigurationRepository) { _internalConfigRepo = internalConfigurationRepository; _fileConfigurationRepository = fileConfigurationRepository; } public int Delay => GetDelay(); private int GetDelay() { var delay = 1000; var fileConfig = _fileConfigurationRepository.Get().GetAwaiter().GetResult(); // sync call, so TODO extend IFileConfigurationPollerOptions interface with 2nd async method if (fileConfig?.Data?.GlobalConfiguration?.ServiceDiscoveryProvider != null && !fileConfig.IsError && fileConfig.Data.GlobalConfiguration.ServiceDiscoveryProvider.PollingInterval > 0) { delay = fileConfig.Data.GlobalConfiguration.ServiceDiscoveryProvider.PollingInterval; } else { var internalConfig = _internalConfigRepo.Get(); if (internalConfig?.Data?.ServiceProviderConfiguration != null && !internalConfig.IsError && internalConfig.Data.ServiceProviderConfiguration.PollingInterval > 0) { delay = internalConfig.Data.ServiceProviderConfiguration.PollingInterval; } } return delay; } } ================================================ FILE: src/Ocelot/Configuration/Repository/DiskFileConfigurationRepository.cs ================================================ using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Responses; using FileSys = System.IO.File; namespace Ocelot.Configuration.Repository; public class DiskFileConfigurationRepository : IFileConfigurationRepository { private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; private FileInfo _ocelotFile; private FileInfo _environmentFile; private readonly object _lock = new(); public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource) { _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; Initialize(AppContext.BaseDirectory); } public DiskFileConfigurationRepository(IWebHostEnvironment hostingEnvironment, IOcelotConfigurationChangeTokenSource changeTokenSource, string folder) { _hostingEnvironment = hostingEnvironment; _changeTokenSource = changeTokenSource; Initialize(folder); } private void Initialize(string folder) { folder ??= AppContext.BaseDirectory; _ocelotFile = new FileInfo(Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile)); var envFile = !string.IsNullOrEmpty(_hostingEnvironment.EnvironmentName) ? string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, _hostingEnvironment.EnvironmentName) : ConfigurationBuilderExtensions.PrimaryConfigFile; _environmentFile = new FileInfo(Path.Combine(folder, envFile)); } public Task> Get() { string jsonConfiguration; lock (_lock) { jsonConfiguration = FileSys.ReadAllText(_environmentFile.FullName); } var fileConfiguration = JsonConvert.DeserializeObject(jsonConfiguration); return Task.FromResult>(new OkResponse(fileConfiguration)); } public Task Set(FileConfiguration fileConfiguration) { var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); lock (_lock) { if (_environmentFile.Exists) { _environmentFile.Delete(); } FileSys.WriteAllText(_environmentFile.FullName, jsonConfiguration); if (_ocelotFile.Exists) { _ocelotFile.Delete(); } FileSys.WriteAllText(_ocelotFile.FullName, jsonConfiguration); } _changeTokenSource.Activate(); return Task.FromResult(new OkResponse()); } } ================================================ FILE: src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs ================================================ using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Logging; namespace Ocelot.Configuration.Repository; public class FileConfigurationPoller : IHostedService, IDisposable { private readonly IOcelotLogger _logger; private readonly IFileConfigurationRepository _repo; private string _previousAsJson; private Timer _timer; private volatile bool _polling; private readonly IFileConfigurationPollerOptions _options; private readonly IInternalConfigurationRepository _internalConfigRepo; private readonly IInternalConfigurationCreator _internalConfigCreator; public FileConfigurationPoller( IOcelotLoggerFactory factory, IFileConfigurationRepository repo, IFileConfigurationPollerOptions options, IInternalConfigurationRepository internalConfigRepo, IInternalConfigurationCreator internalConfigCreator) { _internalConfigRepo = internalConfigRepo; _internalConfigCreator = internalConfigCreator; _options = options; _logger = factory.CreateLogger(); _repo = repo; _previousAsJson = string.Empty; } private void OnTimer(object state) { if (_polling) return; _polling = true; PollAsync().GetAwaiter().GetResult(); // TODO This is not good, TimerCallback must be synchronous _polling = false; } public Task StartAsync(CancellationToken cancellationToken) { if (_timer is not null) return Task.CompletedTask; _logger.LogInformation(() => $"{nameof(FileConfigurationPoller)} is starting."); _timer = new(OnTimer, null, _options.Delay, _options.Delay); // TODO state could be CancellationToken? return Task.CompletedTask; } public Task StopAsync(CancellationToken cancellationToken) { if (_timer is null) return Task.CompletedTask; _logger.LogInformation(() => $"{nameof(FileConfigurationPoller)} is stopping."); _timer.Change(Timeout.Infinite, 0); return Task.CompletedTask; } private async Task PollAsync() { _logger.LogInformation(() => $"{nameof(PollAsync)}: Started polling"); var fileConfig = await _repo.Get(); if (fileConfig.IsError) { _logger.LogWarning(() => $"{nameof(PollAsync)}: Error getting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}"); return; } var asJson = ToJson(fileConfig.Data); if (asJson != _previousAsJson) { var config = await _internalConfigCreator.Create(fileConfig.Data); if (!config.IsError) _internalConfigRepo.AddOrReplace(config.Data); _previousAsJson = asJson; } _logger.LogInformation(() => $"{nameof(PollAsync)}: Finished polling"); } /// /// We could do object comparison here but performance isnt really a problem. This might be an issue one day!. /// /// hash of the config. private static string ToJson(FileConfiguration config) { var currentHash = JsonConvert.SerializeObject(config); return currentHash; } public void Dispose() { _timer?.Dispose(); _timer = null; GC.SuppressFinalize(this); } } ================================================ FILE: src/Ocelot/Configuration/Repository/IFileConfigurationPollerOptions.cs ================================================ namespace Ocelot.Configuration.Repository; public interface IFileConfigurationPollerOptions { int Delay { get; } } ================================================ FILE: src/Ocelot/Configuration/Repository/IFileConfigurationRepository.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Responses; namespace Ocelot.Configuration.Repository; public interface IFileConfigurationRepository { Task> Get(); Task Set(FileConfiguration fileConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Repository/IInternalConfigurationRepository.cs ================================================ using Ocelot.Responses; namespace Ocelot.Configuration.Repository; public interface IInternalConfigurationRepository { Response Get(); Response AddOrReplace(IInternalConfiguration internalConfiguration); } ================================================ FILE: src/Ocelot/Configuration/Repository/InMemoryFileConfigurationPollerOptions.cs ================================================ namespace Ocelot.Configuration.Repository; public class InMemoryFileConfigurationPollerOptions : IFileConfigurationPollerOptions { public int Delay => 1000; } ================================================ FILE: src/Ocelot/Configuration/Repository/InMemoryInternalConfigurationRepository.cs ================================================ using Ocelot.Configuration.ChangeTracking; using Ocelot.Responses; namespace Ocelot.Configuration.Repository; /// /// Register as singleton. /// public class InMemoryInternalConfigurationRepository : IInternalConfigurationRepository { private static readonly object LockObject = new(); private IInternalConfiguration _internalConfiguration; private readonly IOcelotConfigurationChangeTokenSource _changeTokenSource; public InMemoryInternalConfigurationRepository(IOcelotConfigurationChangeTokenSource changeTokenSource) { _changeTokenSource = changeTokenSource; } public Response Get() { return new OkResponse(_internalConfiguration); } public Response AddOrReplace(IInternalConfiguration internalConfiguration) { lock (LockObject) { _internalConfiguration = internalConfiguration; } _changeTokenSource.Activate(); return new OkResponse(); } } ================================================ FILE: src/Ocelot/Configuration/Route.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Values; namespace Ocelot.Configuration; public class Route { public Route() => DownstreamRoute = new(); public Route(bool isDynamic) : this() => IsDynamic = isDynamic; public Route(bool isDynamic, DownstreamRoute route) : this(route) => IsDynamic = isDynamic; public Route(DownstreamRoute route) => DownstreamRoute = [route]; public Route(DownstreamRoute route, HttpMethod method) { DownstreamRoute = [route]; UpstreamHttpMethod = [method]; } public bool IsDynamic { get; } public string Aggregator { get; init; } public List DownstreamRoute { get; init; } public List DownstreamRouteConfig { get; init; } public IDictionary UpstreamHeaderTemplates { get; init; } public string UpstreamHost { get; init; } public HashSet UpstreamHttpMethod { get; init; } public UpstreamPathTemplate UpstreamTemplatePattern { get; init; } } ================================================ FILE: src/Ocelot/Configuration/SecurityOptions.cs ================================================ namespace Ocelot.Configuration; public class SecurityOptions { public SecurityOptions() { IPAllowedList = new List(); IPBlockedList = new List(); } public SecurityOptions(string allowed = null, string blocked = null) : this() { if (!string.IsNullOrEmpty(allowed)) { IPAllowedList.Add(allowed); } if (!string.IsNullOrEmpty(blocked)) { IPBlockedList.Add(blocked); } } public SecurityOptions(IList allowedList = null, IList blockedList = null) { IPAllowedList = allowedList ?? new List(); IPBlockedList = blockedList ?? new List(); } public IList IPAllowedList { get; } public IList IPBlockedList { get; } } ================================================ FILE: src/Ocelot/Configuration/ServiceProviderConfiguration.cs ================================================ namespace Ocelot.Configuration; public class ServiceProviderConfiguration { public string Scheme { get; init; } public string Host { get; init; } public int Port { get; init; } public string Type { get; init; } public string Token { get; init; } public string ConfigurationKey { get; init; } public int PollingInterval { get; init; } public string Namespace { get; init; } } ================================================ FILE: src/Ocelot/Configuration/Setter/FileAndInternalConfigurationSetter.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Responses; namespace Ocelot.Configuration.Setter; public class FileAndInternalConfigurationSetter : IFileConfigurationSetter { private readonly IInternalConfigurationRepository _internalConfigRepo; private readonly IInternalConfigurationCreator _configCreator; private readonly IFileConfigurationRepository _repo; public FileAndInternalConfigurationSetter( IInternalConfigurationRepository configRepo, IInternalConfigurationCreator configCreator, IFileConfigurationRepository repo) { _internalConfigRepo = configRepo; _configCreator = configCreator; _repo = repo; } public async Task Set(FileConfiguration fileConfig) { var response = await _repo.Set(fileConfig); if (response.IsError) { return new ErrorResponse(response.Errors); } var config = await _configCreator.Create(fileConfig); if (!config.IsError) { _internalConfigRepo.AddOrReplace(config.Data); } return new ErrorResponse(config.Errors); } } ================================================ FILE: src/Ocelot/Configuration/Setter/IFileConfigurationSetter.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Responses; namespace Ocelot.Configuration.Setter; public interface IFileConfigurationSetter { Task Set(FileConfiguration config); } ================================================ FILE: src/Ocelot/Configuration/Validator/ConfigurationValidationResult.cs ================================================ using Ocelot.Errors; namespace Ocelot.Configuration.Validator; public class ConfigurationValidationResult { public ConfigurationValidationResult(bool isError) { IsError = isError; Errors = new List(); } public ConfigurationValidationResult(bool isError, List errors) { IsError = isError; Errors = errors; } public bool IsError { get; } public List Errors { get; } } ================================================ FILE: src/Ocelot/Configuration/Validator/FileAuthenticationOptionsValidator.cs ================================================ using FluentValidation; using Microsoft.AspNetCore.Authentication; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; namespace Ocelot.Configuration.Validator; public class FileAuthenticationOptionsValidator : AbstractValidator { private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider; public FileAuthenticationOptionsValidator(IAuthenticationSchemeProvider authenticationSchemeProvider) { _authenticationSchemeProvider = authenticationSchemeProvider; RuleFor(authOptions => authOptions) .MustAsync(IsSupportedAuthenticationProviders) .WithMessage($"{nameof(FileRoute.AuthenticationOptions)}: {{PropertyValue}} is unsupported authentication provider"); } private async Task IsSupportedAuthenticationProviders(FileAuthenticationOptions options, CancellationToken cancellationToken) { var keys = options.AuthenticationProviderKeys; if (options.AuthenticationProviderKey.IsEmpty() && (keys is null || keys.Length == 0)) { return true; } var schemes = await _authenticationSchemeProvider.GetAllSchemesAsync(); var supportedSchemes = schemes.Select(scheme => scheme.Name); var primary = options.AuthenticationProviderKey; return !string.IsNullOrWhiteSpace(primary) && supportedSchemes.Contains(primary) || (string.IsNullOrWhiteSpace(primary) && keys.All(supportedSchemes.Contains)); } } ================================================ FILE: src/Ocelot/Configuration/Validator/FileConfigurationFluentValidator.cs ================================================ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.Errors; using Ocelot.Infrastructure; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Configuration.Validator; /// Validation of a objects. public partial class FileConfigurationFluentValidator : AbstractValidator, IConfigurationValidator { private readonly List _serviceDiscoveryFinderDelegates; public FileConfigurationFluentValidator(IServiceProvider provider, RouteFluentValidator routeFluentValidator, FileGlobalConfigurationFluentValidator fileGlobalConfigurationFluentValidator) { _serviceDiscoveryFinderDelegates = provider .GetServices() .ToList(); RuleForEach(configuration => configuration.Routes) .SetValidator(routeFluentValidator); RuleFor(configuration => configuration.GlobalConfiguration) .SetValidator(fileGlobalConfigurationFluentValidator); RuleForEach(configuration => configuration.Routes) .Must((config, route) => IsNotDuplicateIn(route, config.Routes)) .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicate"); RuleForEach(configuration => configuration.Routes) .Must((config, route) => HaveServiceDiscoveryProviderRegistered(route, config.GlobalConfiguration.ServiceDiscoveryProvider)) .WithMessage((_, _) => "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); RuleForEach(configuration => configuration.Routes) .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.UpstreamPathTemplate)) .WithMessage((_, route) => $"{nameof(route.UpstreamPathTemplate)} '{route.UpstreamPathTemplate}' has duplicated placeholder"); RuleForEach(configuration => configuration.Routes) .Must((_, route) => IsPlaceholderNotDuplicatedIn(route.DownstreamPathTemplate)) .WithMessage((_, route) => $"{nameof(route.DownstreamPathTemplate)} '{route.DownstreamPathTemplate}' has duplicated placeholder"); RuleFor(configuration => configuration.GlobalConfiguration.ServiceDiscoveryProvider) .Must(HaveServiceDiscoveryProviderRegistered) .WithMessage((_, _) => "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); RuleForEach(configuration => configuration.Routes) .Must((config, route) => IsNotDuplicateIn(route, config.Aggregates)) .WithMessage((_, route) => $"{nameof(route)} {route.UpstreamPathTemplate} has duplicate aggregate"); RuleForEach(configuration => configuration.Aggregates) .Must((config, aggregateRoute) => IsNotDuplicateIn(aggregateRoute, config.Aggregates)) .WithMessage((_, aggregate) => $"{nameof(aggregate)} {aggregate.UpstreamPathTemplate} has duplicate aggregate"); RuleForEach(configuration => configuration.Aggregates) .Must((config, aggregateRoute) => AllRoutesForAggregateExist(aggregateRoute, config.Routes)) .WithMessage((_, aggregateRoute) => $"Routes for {nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} either do not exist or do not have correct ServiceName property"); RuleForEach(configuration => configuration.Aggregates) .Must((config, aggregateRoute) => DoesNotContainRoutesWithSpecificRequestIdKeys(aggregateRoute, config.Routes)) .WithMessage((_, aggregateRoute) => $"{nameof(aggregateRoute)} {aggregateRoute.UpstreamPathTemplate} contains Route with specific RequestIdKey, this is not possible with Aggregates"); } private const string ServiceFabric = ServiceFabricServiceDiscoveryProvider.Type; private bool HaveServiceDiscoveryProviderRegistered(FileRoute route, FileServiceDiscoveryProvider serviceDiscoveryProvider) { return string.IsNullOrEmpty(route.ServiceName) || ServiceFabric.Equals(serviceDiscoveryProvider?.Type, StringComparison.OrdinalIgnoreCase) || _serviceDiscoveryFinderDelegates.Any(); } private bool HaveServiceDiscoveryProviderRegistered(FileServiceDiscoveryProvider serviceDiscoveryProvider) { return serviceDiscoveryProvider == null || ServiceFabric.Equals(serviceDiscoveryProvider.Type, StringComparison.OrdinalIgnoreCase) || string.IsNullOrEmpty(serviceDiscoveryProvider.Type) || _serviceDiscoveryFinderDelegates.Any(); } public async Task> IsValid(FileConfiguration configuration) { var validateResult = await ValidateAsync(configuration); if (validateResult.IsValid) { return new OkResponse(new ConfigurationValidationResult(false)); } var errors = validateResult.Errors.Select(failure => new FileValidationFailedError(failure.ErrorMessage)); var result = new ConfigurationValidationResult(true, errors.Cast().ToList()); return new OkResponse(result); } private static bool AllRoutesForAggregateExist(FileAggregateRoute fileAggregateRoute, List routes) { var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key)); return routesForAggregate.Count() == fileAggregateRoute.RouteKeys.Count; } [GeneratedRegex(@"\{\w+\}", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")] private static partial Regex PlaceholderRegex(); private static bool IsPlaceholderNotDuplicatedIn(string pathTemplate) { var placeholders = PlaceholderRegex().Matches(pathTemplate) .Select(m => m.Value).ToList(); return placeholders.Count == placeholders.Distinct().Count(); } private static bool DoesNotContainRoutesWithSpecificRequestIdKeys(FileAggregateRoute fileAggregateRoute, IEnumerable routes) { var routesForAggregate = routes.Where(r => fileAggregateRoute.RouteKeys.Contains(r.Key)); return routesForAggregate.All(r => string.IsNullOrEmpty(r.RequestIdKey)); } private static bool IsNotDuplicateIn(FileRoute route, IEnumerable routes) { var matchingRoutes = routes .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate && r.UpstreamHost == route.UpstreamHost && AreTheSame(r.UpstreamHeaderTemplates, route.UpstreamHeaderTemplates)) .ToArray(); if (matchingRoutes.Length == 1) { return true; } var allowAllVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count == 0); var duplicateAllowAllVerbs = matchingRoutes.Count(x => x.UpstreamHttpMethod.Count == 0) > 1; var specificVerbs = matchingRoutes.Any(x => x.UpstreamHttpMethod.Count != 0); var duplicateSpecificVerbs = matchingRoutes.SelectMany(x => x.UpstreamHttpMethod).GroupBy(x => x.ToLower()).SelectMany(x => x.Skip(1)).Any(); if (duplicateAllowAllVerbs || duplicateSpecificVerbs || allowAllVerbs && specificVerbs) { return false; } return true; } private static bool AreTheSame(IDictionary upstreamHeaderTemplates, IDictionary otherHeaderTemplates) => upstreamHeaderTemplates.Count == otherHeaderTemplates.Count && upstreamHeaderTemplates.All(x => otherHeaderTemplates.ContainsKey(x.Key) && otherHeaderTemplates[x.Key] == x.Value); private static bool IsNotDuplicateIn(FileRoute route, IEnumerable aggregateRoutes) { var duplicate = aggregateRoutes .Any(a => a.UpstreamPathTemplate == route.UpstreamPathTemplate && a.UpstreamHost == route.UpstreamHost && route.UpstreamHttpMethod.Select(x => x.ToLower()).Contains("get")); return !duplicate; } private static bool IsNotDuplicateIn(FileAggregateRoute route, IEnumerable aggregateRoutes) { var matchingRoutes = aggregateRoutes .Where(r => r.UpstreamPathTemplate == route.UpstreamPathTemplate & r.UpstreamHost == route.UpstreamHost); return matchingRoutes.Count() <= 1; } } ================================================ FILE: src/Ocelot/Configuration/Validator/FileGlobalConfigurationFluentValidator.cs ================================================ using FluentValidation; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Validator; public class FileGlobalConfigurationFluentValidator : AbstractValidator { public FileGlobalConfigurationFluentValidator( FileQoSOptionsFluentValidator qosValidator, FileAuthenticationOptionsValidator authValidator) { RuleFor(configuration => configuration.QoSOptions) .SetValidator(qosValidator); RuleFor(configuration => configuration.AuthenticationOptions) .SetValidator(authValidator); } } ================================================ FILE: src/Ocelot/Configuration/Validator/FileQoSOptionsFluentValidator.cs ================================================ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.QualityOfService; namespace Ocelot.Configuration.Validator; public class FileQoSOptionsFluentValidator : AbstractValidator { private readonly QosDelegatingHandlerDelegate _qosDelegatingHandlerDelegate; public FileQoSOptionsFluentValidator(IServiceProvider provider) { _qosDelegatingHandlerDelegate = provider.GetService(); When(UseQos, CheckRules); } private bool UseQos(FileQoSOptions opts) => new QoSOptions(opts).UseQos; private void CheckRules() { RuleFor(qos => qos) .Must(HaveQosHandlerRegistered) .WithMessage($"Unable to start Ocelot because either a {nameof(Route)} or {nameof(FileConfiguration.GlobalConfiguration)} are using {nameof(FileRoute.QoSOptions)} but no {nameof(QosDelegatingHandlerDelegate)} has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); } private bool HaveQosHandlerRegistered(FileQoSOptions arg) { return _qosDelegatingHandlerDelegate != null; } } ================================================ FILE: src/Ocelot/Configuration/Validator/FileValidationFailedError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Configuration.Validator; public class FileValidationFailedError : Error { public FileValidationFailedError(string message) : base(message, OcelotErrorCode.FileValidationFailedError, 404) { } } ================================================ FILE: src/Ocelot/Configuration/Validator/HostAndPortValidator.cs ================================================ using FluentValidation; using Ocelot.Configuration.File; namespace Ocelot.Configuration.Validator; public class HostAndPortValidator : AbstractValidator { public HostAndPortValidator() { RuleFor(r => r.Host) .NotEmpty() .WithMessage("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); } } ================================================ FILE: src/Ocelot/Configuration/Validator/IConfigurationValidator.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Responses; namespace Ocelot.Configuration.Validator; public interface IConfigurationValidator { Task> IsValid(FileConfiguration configuration); } ================================================ FILE: src/Ocelot/Configuration/Validator/RouteFluentValidator.cs ================================================ using FluentValidation; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Infrastructure; namespace Ocelot.Configuration.Validator; /// /// Default implementation od the abstract class. /// public partial class RouteFluentValidator : AbstractValidator { public RouteFluentValidator( HostAndPortValidator hostAndPortValidator, FileQoSOptionsFluentValidator qosOptsValidator, FileAuthenticationOptionsValidator authOptsValidator) { RuleFor(route => route.QoSOptions) .SetValidator(qosOptsValidator); RuleFor(route => route.DownstreamPathTemplate) .NotEmpty() .WithMessage("{PropertyName} cannot be empty"); RuleFor(route => route.UpstreamPathTemplate) .NotEmpty() .WithMessage("{PropertyName} cannot be empty"); When(route => !string.IsNullOrEmpty(route.DownstreamPathTemplate), () => { RuleFor(route => route.DownstreamPathTemplate) .Must(path => path.StartsWith('/')) .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); RuleFor(route => route.DownstreamPathTemplate) .Must(path => !path.Contains("//")) .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); RuleFor(route => route.DownstreamPathTemplate) .Must(path => !path.Contains("https://") && !path.Contains("http://")) .WithMessage("{PropertyName} {PropertyValue} contains scheme"); }); When(route => !string.IsNullOrEmpty(route.UpstreamPathTemplate), () => { RuleFor(route => route.UpstreamPathTemplate) .Must(path => !path.Contains("//")) .WithMessage("{PropertyName} {PropertyValue} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); RuleFor(route => route.UpstreamPathTemplate) .Must(path => path.StartsWith('/')) .WithMessage("{PropertyName} {PropertyValue} doesnt start with forward slash"); RuleFor(route => route.UpstreamPathTemplate) .Must(path => !path.Contains("https://") && !path.Contains("http://")) .WithMessage("{PropertyName} {PropertyValue} contains scheme"); }); When(route => route.RateLimitOptions != null && route.RateLimitOptions.EnableRateLimiting != false, () => { RuleFor(route => route.RateLimitOptions.Limit) .Must(limit => !limit.HasValue || (limit.HasValue && limit.Value > 0)) .WithMessage(route => $"RateLimitOptions.Limit is negative or zero for the route {route}"); RuleFor(route => route.RateLimitOptions.Period) .NotEmpty() .WithMessage("RateLimitOptions.Period is empty"); RuleFor(route => route.RateLimitOptions) .Must(IsValidPeriod) .WithMessage("RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); }); RuleFor(route => route.AuthenticationOptions) .SetValidator(authOptsValidator); When(route => string.IsNullOrEmpty(route.ServiceName), () => { RuleFor(r => r.DownstreamHostAndPorts).NotEmpty() .WithMessage("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); }); When(route => string.IsNullOrEmpty(route.ServiceName), () => { RuleForEach(route => route.DownstreamHostAndPorts) .SetValidator(hostAndPortValidator); }); When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersion), () => { RuleFor(r => r.DownstreamHttpVersion).Matches("^[0-9]([.,][0-9]{1,1})?$"); }); When(route => !string.IsNullOrEmpty(route.DownstreamHttpVersionPolicy), () => { RuleFor(r => r.DownstreamHttpVersionPolicy).Matches($@"^({VersionPolicies.RequestVersionExact}|{VersionPolicies.RequestVersionOrHigher}|{VersionPolicies.RequestVersionOrLower})$"); }); } [GeneratedRegex(@"^\d+(\.\d+)?ms", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex MilliSecondsRegex(); [GeneratedRegex(@"^\d+(\.\d+)?s", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex SecondsRegex(); [GeneratedRegex(@"^\d+(\.\d+)?m", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex MinutesRegex(); [GeneratedRegex(@"^\d+(\.\d+)?h", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex HoursRegex(); [GeneratedRegex(@"^\d+(\.\d+)?d", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex DaysRegex(); private static bool IsValidPeriod(FileRateLimitByHeaderRule rateLimitOptions) { if (string.IsNullOrEmpty(rateLimitOptions.Period)) { return false; } var period = rateLimitOptions.Period.Trim(); return MilliSecondsRegex().Match(period).Success || SecondsRegex().Match(period).Success || MinutesRegex().Match(period).Success || HoursRegex().Match(period).Success || DaysRegex().Match(period).Success; } } ================================================ FILE: src/Ocelot/DependencyInjection/ConfigurationBuilderExtensions.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.Infrastructure; namespace Ocelot.DependencyInjection; /// /// Defines extension-methods for the interface. /// public static partial class ConfigurationBuilderExtensions { public const string PrimaryConfigFile = "ocelot.json"; public const string GlobalConfigFile = "ocelot.global.json"; public const string EnvironmentConfigFile = "ocelot.{0}.json"; [Obsolete("Please set BaseUrl in ocelot.json GlobalConfiguration.BaseUrl")] public static IConfigurationBuilder AddOcelotBaseUrl(this IConfigurationBuilder builder, string baseUrl) { var memorySource = new MemoryConfigurationSource { InitialData = new List> { new("BaseUrl", baseUrl), }, }; return builder.Add(memorySource); } /// /// Adds Ocelot configuration by environment, reading the required files from the default path. /// /// Configuration builder to extend. /// Web hosting environment object. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env) => builder.AddOcelot(".", env); /// /// Adds Ocelot configuration by environment, reading the required files from the specified folder. /// /// Configuration builder to extend. /// Folder to read files from. /// Web hosting environment object. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env) => builder.AddOcelot(folder, env, MergeOcelotJson.ToFile); /// /// Adds Ocelot configuration by environment and merge option, reading the required files from the current default folder. /// /// Use optional arguments for injections and overridings. /// Configuration builder to extend. /// Web hosting environment object. /// Option to merge files to. /// Primary config file. /// Global config file. /// Environment config file. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections => builder.AddOcelot(".", env, mergeTo, primaryConfigFile, globalConfigFile, environmentConfigFile, optional, reloadOnChange); /// /// Adds Ocelot configuration by environment and merge option, reading the required files from the specified folder. /// /// Use optional arguments for injections and overridings. /// Configuration builder to extend. /// Folder to read files from. /// Web hosting environment object. /// Option to merge files to. /// Primary config file. /// Global config file. /// Environment config file. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string folder, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { var json = GetMergedOcelotJson(folder, env, null, primaryConfigFile, globalConfigFile, environmentConfigFile); primaryConfigFile ??= Path.Join(folder, PrimaryConfigFile); // if not specified, merge & write back to the same folder return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); } private static IConfigurationBuilder ApplyMergeOcelotJsonOption(IConfigurationBuilder builder, MergeOcelotJson mergeTo, string json, string primaryConfigFile, bool? optional, bool? reloadOnChange) { return mergeTo == MergeOcelotJson.ToMemory ? builder.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(json))) : AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); } [GeneratedRegex(@"^ocelot\.(.*?)\.json$", RegexOptions.IgnoreCase | RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds, "en-US")] private static partial Regex SubConfigRegex(); private static string GetMergedOcelotJson(string folder, IWebHostEnvironment env, FileConfiguration fileConfiguration = null, string primaryFile = null, string globalFile = null, string environmentFile = null) { // All versions of overloaded AddOcelot methods call this GetMergedOcelotJson one, so we improve Regex performance by cache increasing. // Developers can adjust the RegexGlobal value BEFORE calling AddOcelot // Developers can adjust the Regex.CacheSize value AFTER calling AddOcelot Regex.CacheSize = RegexGlobal.RegexCacheSize; var envName = string.IsNullOrEmpty(env?.EnvironmentName) ? "Development" : env.EnvironmentName; environmentFile ??= Path.Join(folder, string.Format(EnvironmentConfigFile, envName)); var reg = SubConfigRegex(); var environmentFileInfo = new FileInfo(environmentFile); var files = new DirectoryInfo(folder) .EnumerateFiles() .Where(fi => reg.IsMatch(fi.Name) && !fi.Name.Equals(environmentFileInfo.Name, StringComparison.OrdinalIgnoreCase) && !fi.FullName.Equals(environmentFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) .ToArray(); fileConfiguration ??= new FileConfiguration(); primaryFile ??= Path.Join(folder, PrimaryConfigFile); globalFile ??= Path.Join(folder, GlobalConfigFile); var primaryFileInfo = new FileInfo(primaryFile); var globalFileInfo = new FileInfo(globalFile); foreach (var file in files) { if (files.Length > 1 && file.Name.Equals(primaryFileInfo.Name, StringComparison.OrdinalIgnoreCase) && file.FullName.Equals(primaryFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { continue; } var lines = File.ReadAllText(file.FullName); var config = JsonConvert.DeserializeObject(lines); if (file.Name.Equals(globalFileInfo.Name, StringComparison.OrdinalIgnoreCase) && file.FullName.Equals(globalFileInfo.FullName, StringComparison.OrdinalIgnoreCase)) { fileConfiguration.GlobalConfiguration = config.GlobalConfiguration; } fileConfiguration.Aggregates.AddRange(config.Aggregates); fileConfiguration.Routes.AddRange(config.Routes); } return JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); } /// /// Adds Ocelot configuration by ready configuration object and writes JSON to the primary configuration file.
/// Finally, adds JSON file as configuration provider. ///
/// Use optional arguments for injections and overridings. /// Configuration builder to extend. /// File configuration to add as JSON provider. /// Primary config file. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, string primaryConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { var json = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); return AddOcelotJsonFile(builder, json, primaryConfigFile, optional, reloadOnChange); } /// /// Adds Ocelot configuration by ready configuration object, environment and merge option, reading the required files from the current default folder. /// /// Configuration builder to extend. /// File configuration to add as JSON provider. /// Web hosting environment object. /// Option to merge files to. /// Primary config file. /// Global config file. /// Environment config file. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, FileConfiguration fileConfiguration, IWebHostEnvironment env, MergeOcelotJson mergeTo, string primaryConfigFile = null, string globalConfigFile = null, string environmentConfigFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { var json = GetMergedOcelotJson(".", env, fileConfiguration, primaryConfigFile, globalConfigFile, environmentConfigFile); return ApplyMergeOcelotJsonOption(builder, mergeTo, json, primaryConfigFile, optional, reloadOnChange); } /// /// Adds Ocelot primary configuration file (aka ocelot.json).
/// Writes JSON to the file.
/// Adds the file as a JSON configuration provider via the extension. ///
/// Use optional arguments for injections and overridings. /// The builder to extend. /// JSON data of the Ocelot configuration. /// Primary config file. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. private static IConfigurationBuilder AddOcelotJsonFile(IConfigurationBuilder builder, string json, string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections { var primary = primaryFile ?? PrimaryConfigFile; File.WriteAllText(primary, json); return builder?.AddJsonFile(primary, optional ?? false, reloadOnChange ?? false); } /// /// Adds Ocelot primary configuration file (aka ocelot.json) in read-only mode. /// Adds the file as a JSON configuration provider via the extension. /// /// Use optional arguments for injections and overridings. /// The builder to extend. /// Primary config file path. /// The 2nd argument of the AddJsonFile. /// The 3rd argument of the AddJsonFile. /// An object. public static IConfigurationBuilder AddOcelot(this IConfigurationBuilder builder, string primaryFile = null, bool? optional = null, bool? reloadOnChange = null) // optional injections => builder.AddJsonFile(primaryFile ?? PrimaryConfigFile, optional ?? false, reloadOnChange ?? false); } ================================================ FILE: src/Ocelot/DependencyInjection/Features.cs ================================================ using FluentValidation; using Microsoft.Extensions.DependencyInjection; using Ocelot.Cache; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.Logging; using Ocelot.RateLimiting; namespace Ocelot.DependencyInjection; public static class Features { /// This Ocelot Core feature adds validation for JSON configuration File-models. /// Added validator-classes must implement the interface, where T is File-model. /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddConfigurationValidators(this IServiceCollection services) => services .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton(); /// /// Ocelot feature: Rate Limiting. /// /// /// Read The Docs: Rate Limiting. /// /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddOcelotRateLimiting(this IServiceCollection services) => services .AddSingleton() .AddSingleton(); /// /// Ocelot feature: Request Caching. /// /// /// Read The Docs: Caching. /// /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddOcelotCache(this IServiceCollection services) => services .AddSingleton, DefaultMemoryCache>() .AddSingleton, DefaultMemoryCache>() .AddSingleton, DefaultMemoryCache>() .AddSingleton() .AddSingleton() .AddMemoryCache(); /// /// Ocelot feature: Routing based on request header. /// /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddOcelotHeaderRouting(this IServiceCollection services) => services .AddSingleton() .AddSingleton() .AddSingleton(); public static IServiceCollection AddOcelotLogging(this IServiceCollection services) => services .AddSingleton() .AddSingleton() .AddLogging(); /// /// Ocelot feature: Inject custom metadata and use it in delegating handlers. /// /// The services collection to add the feature to. /// The same object. public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => services.AddSingleton(); } ================================================ FILE: src/Ocelot/DependencyInjection/IOcelotBuilder.cs ================================================ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Multiplexer; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.DependencyInjection; public interface IOcelotBuilder { IServiceCollection Services { get; } IConfiguration Configuration { get; } IMvcCoreBuilder MvcCoreBuilder { get; } /// /// Adds a of the type as a transient service, with the option to make the handler globally available. /// /// The type of a to be registered. /// True if the handler should be globally available. /// The reference to the same object. /// Generates an exception if the type does not inherit from the . IOcelotBuilder AddDelegatingHandler(Type delegateType, bool global = false); /// /// Adds a of the type as a transient service, with the option to make the handler globally available. /// /// The type of a to be registered. /// True if the handler should be globally available. /// The reference to the same object. IOcelotBuilder AddDelegatingHandler(bool global = false) where THandler : DelegatingHandler; IOcelotBuilder AddSingletonDefinedAggregator() where T : class, IDefinedAggregator; IOcelotBuilder AddTransientDefinedAggregator() where T : class, IDefinedAggregator; IOcelotBuilder AddCustomLoadBalancer() where T : ILoadBalancer, new(); IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer( Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddCustomLoadBalancer( Func loadBalancerFactoryFunc) where T : ILoadBalancer; IOcelotBuilder AddConfigPlaceholders(); } ================================================ FILE: src/Ocelot/DependencyInjection/MergeOcelotJson.cs ================================================ namespace Ocelot.DependencyInjection; public enum MergeOcelotJson { /// /// The option to merge all configuration files to one primary config file aka ocelot.json. /// ToFile = 0, /// /// The option to merge all configuration files to memory and reuse the config by in-memory configuration provider. /// ToMemory = 1, } ================================================ FILE: src/Ocelot/DependencyInjection/OcelotBuilder.cs ================================================ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Ocelot.Administration; using Ocelot.Authorization; using Ocelot.Claims; using Ocelot.Configuration; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Parser; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Creators; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Multiplexer; using Ocelot.PathManipulation; using Ocelot.QualityOfService; using Ocelot.QueryStrings; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; using Ocelot.Requester; using Ocelot.Responder; using Ocelot.Security; using Ocelot.Security.IPSecurity; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using Ocelot.WebSockets; using System.Reflection; namespace Ocelot.DependencyInjection; public class OcelotBuilder : IOcelotBuilder { public IServiceCollection Services { get; } public IConfiguration Configuration { get; } public IMvcCoreBuilder MvcCoreBuilder { get; } public OcelotBuilder(IServiceCollection services, IConfiguration configurationRoot, Func customBuilder = null) { Configuration = configurationRoot; Services = services; Services.Configure(configurationRoot); Services.Configure(configurationRoot.GetSection(nameof(FileConfiguration.GlobalConfiguration))); Services.AddConfigurationValidators(); // based on the AbstractValidator interface Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); // Chinese developers should read StackOverflow ignoring Microsoft Learn docs -> http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc Services.AddHttpContextAccessor(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); // Add security Services.TryAddSingleton(); Services.TryAddSingleton(); // Features Services.AddOcelotCache(); Services.AddOcelotHeaderRouting(); Services.AddOcelotLogging(); Services.AddOcelotMessageInvokerPool(); Services.AddOcelotMetadata(); Services.AddOcelotRateLimiting(); // Add ASP.NET services var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly; MvcCoreBuilder = (customBuilder ?? AddDefaultAspNetServices) .Invoke(Services.AddMvcCore(), assembly); } /// /// Adds default ASP.NET services which are the minimal part of the gateway core. /// /// Finally the builder adds Newtonsoft.Json services via the extension-method.
/// To remove these services, use custom builder in the extension-method. ///
///
/// /// Note that the following extensions being called:
/// - , impossible to remove.
/// -
/// - . /// /// Warning! The following extensions being called:
/// -
/// -
/// -
/// - , removable. ///
///
/// The default builder being returned by extension-method. /// The web app assembly. /// An object. protected IMvcCoreBuilder AddDefaultAspNetServices(IMvcCoreBuilder builder, Assembly assembly) { Services .AddMiddlewareAnalysis() .AddWebEncoders(); return builder .AddApplicationPart(assembly) .AddControllersAsServices() .AddAuthorization() .AddNewtonsoftJson(); } public IOcelotBuilder AddSingletonDefinedAggregator() where T : class, IDefinedAggregator { Services.AddSingleton(); return this; } public IOcelotBuilder AddTransientDefinedAggregator() where T : class, IDefinedAggregator { Services.AddTransient(); return this; } public IOcelotBuilder AddCustomLoadBalancer() where TLoadBalancer : ILoadBalancer, new() { static TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider) => new(); // TODO Not tested by acceptance tests, Assert another constructors with injected params? return AddCustomLoadBalancer(Create); } public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where TLoadBalancer : ILoadBalancer { TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider) => loadBalancerFactoryFunc(); return AddCustomLoadBalancer(Create); } public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where TLoadBalancer : ILoadBalancer { TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider) => loadBalancerFactoryFunc(provider); return AddCustomLoadBalancer(Create); } public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where TLoadBalancer : ILoadBalancer { TLoadBalancer Create(IServiceProvider provider, DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider) => loadBalancerFactoryFunc(route, discoveryProvider); return AddCustomLoadBalancer(Create); } public IOcelotBuilder AddCustomLoadBalancer(Func loadBalancerFactoryFunc) where TLoadBalancer : ILoadBalancer { ILoadBalancer Create(DownstreamRoute route, IServiceDiscoveryProvider discoveryProvider) => loadBalancerFactoryFunc(_serviceProvider, route, discoveryProvider); ILoadBalancerCreator implementationFactory(IServiceProvider provider) { _serviceProvider = provider; return new DelegateInvokingLoadBalancerCreator(Create); } Services.AddSingleton(implementationFactory); return this; } /// /// Adds a of the type as a transient service, with the option to make the handler globally available. /// /// The type of a to be registered. /// True if the handler should be globally available. /// The reference to the same object. /// Generates an exception if the type does not inherit from the . public IOcelotBuilder AddDelegatingHandler(Type delegateType, bool global = false) { if (!typeof(DelegatingHandler).IsAssignableFrom(delegateType)) { throw new ArgumentOutOfRangeException(nameof(delegateType), delegateType.Name, "It is not a delegating handler"); } if (global) { Services.AddTransient(delegateType); Services.AddTransient(provider => { var service = provider.GetService(delegateType) as DelegatingHandler; return new GlobalDelegatingHandler(service); }); } else { Services.AddTransient(typeof(DelegatingHandler), delegateType); } return this; } /// /// Adds a of the type as a transient service, with the option to make the handler globally available. /// /// The type of a to be registered. /// True if the handler should be globally available. /// The reference to the same object. public IOcelotBuilder AddDelegatingHandler(bool global = false) where THandler : DelegatingHandler { if (global) { Services.AddTransient(); Services.AddTransient(provider => { var service = provider.GetService(); return new GlobalDelegatingHandler(service); }); } else { Services.AddTransient(); } return this; } public IOcelotBuilder AddConfigPlaceholders() { // see: https://greatrexpectations.com/2018/10/25/decorators-in-net-core-with-dependency-injection var wrappedDescriptor = Services.First(x => x.ServiceType == typeof(IPlaceholders)); var objectFactory = ActivatorUtilities.CreateFactory( typeof(ConfigAwarePlaceholders), new[] { typeof(IPlaceholders) }); Services.Replace(ServiceDescriptor.Describe( typeof(IPlaceholders), provider => (IPlaceholders)objectFactory( provider, new[] { CreateInstance(provider, wrappedDescriptor) }), wrappedDescriptor.Lifetime )); return this; } /// For local implementation purposes, so it MUST NOT be public!.. private IServiceProvider _serviceProvider; // TODO Reuse ActivatorUtilities factories? private static object CreateInstance(IServiceProvider provider, ServiceDescriptor descriptor) { if (descriptor.ImplementationInstance != null) { return descriptor.ImplementationInstance; } if (descriptor.ImplementationFactory != null) { return descriptor.ImplementationFactory(provider); } return ActivatorUtilities.GetServiceOrCreateInstance(provider, descriptor.ImplementationType); } } ================================================ FILE: src/Ocelot/DependencyInjection/ServiceCollectionExtensions.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using System.Reflection; namespace Ocelot.DependencyInjection; public static class ServiceCollectionExtensions { /// /// Adds default ASP.NET services and Ocelot application services.
/// Creates default from the current service descriptors. /// If the configuration is not registered, it will try to read ocelot configuration from current working directory. ///
/// /// Remarks for default ASP.NET services being injected see in docs of the method. /// /// Current services collection. /// An object. public static IOcelotBuilder AddOcelot(this IServiceCollection services) { var configuration = services.FindConfiguration(null); return new OcelotBuilder(services, configuration); } /// /// Adds default ASP.NET services and Ocelot application services with configuration. /// /// /// Remarks for default ASP.NET services will be injected, see docs of the method. /// /// Current services collection. /// Current web app configuration. /// An object. public static IOcelotBuilder AddOcelot(this IServiceCollection services, IConfiguration configuration) { return new OcelotBuilder(services, configuration); } /// /// Adds Ocelot application services and custom ASP.NET services with custom builder.
/// Creates default from the current service descriptors. /// If the configuration is not registered, it will try to read ocelot configuration from current working directory. ///
/// /// Warning! To understand which ASP.NET services should be injected/removed by custom builder, see docs of the method. /// /// Current services collection. /// Current custom builder for ASP.NET MVC pipeline. /// An object. [Obsolete("Use AddOcelotUsingBuilder() overloaded version with the 'IConfiguration configuration' parameter.")] public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, Func customBuilder) { var configuration = services.FindConfiguration(null); return new OcelotBuilder(services, configuration, customBuilder); } /// /// Adds Ocelot application services and custom ASP.NET services with configuration and custom builder. /// /// /// Warning! To understand which ASP.NET services should be injected/removed by custom builder, see docs of the method. /// /// Current services collection. /// Current web app configuration. /// Current custom builder for ASP.NET MVC pipeline. /// An object. public static IOcelotBuilder AddOcelotUsingBuilder(this IServiceCollection services, IConfiguration configuration, Func customBuilder) { configuration ??= services.FindConfiguration(null); return new OcelotBuilder(services, configuration, customBuilder); } private static IConfiguration DefaultConfiguration(IWebHostEnvironment env) => new ConfigurationBuilder().AddOcelot(env).Build(); private static IConfiguration FindConfiguration(this IServiceCollection services, IWebHostEnvironment env) { var descriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IConfiguration)); if (descriptor == null) { return DefaultConfiguration(env); } var provider = new ServiceCollection().Add(descriptor).BuildServiceProvider(true); var configuration = provider.GetService(); return configuration ?? DefaultConfiguration(env); } } ================================================ FILE: src/Ocelot/DownstreamPathManipulation/ChangeDownstreamPathTemplate.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using Ocelot.Values; using System.Security.Claims; namespace Ocelot.PathManipulation; public class ChangeDownstreamPathTemplate : IChangeDownstreamPathTemplate { private readonly IClaimsParser _claimsParser; public ChangeDownstreamPathTemplate(IClaimsParser claimsParser) { _claimsParser = claimsParser; } public Response ChangeDownstreamPath(List claimsToThings, IEnumerable claims, DownstreamPathTemplate downstreamPathTemplate, List placeholders) { foreach (var config in claimsToThings) { var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index); if (value.IsError) { return new ErrorResponse(value.Errors); } var placeholderName = $"{{{config.ExistingKey}}}"; if (!downstreamPathTemplate.Value.Contains(placeholderName)) { return new ErrorResponse(new CouldNotFindPlaceholderError(placeholderName)); } if (placeholders.Any(ph => ph.Name == placeholderName)) { placeholders.RemoveAll(ph => ph.Name == placeholderName); } placeholders.Add(new PlaceholderNameAndValue(placeholderName, value.Data)); } return new OkResponse(); } } ================================================ FILE: src/Ocelot/DownstreamPathManipulation/IChangeDownstreamPathTemplate.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; using System.Security.Claims; namespace Ocelot.PathManipulation; public interface IChangeDownstreamPathTemplate { Response ChangeDownstreamPath(List claimsToThings, IEnumerable claims, DownstreamPathTemplate downstreamPathTemplate, List placeholders); } ================================================ FILE: src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.PathManipulation; namespace Ocelot.DownstreamPathManipulation.Middleware; public class ClaimsToDownstreamPathMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IChangeDownstreamPathTemplate _changeDownstreamPathTemplate; public ClaimsToDownstreamPathMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IChangeDownstreamPathTemplate changeDownstreamPathTemplate) : base(loggerFactory.CreateLogger()) { _next = next; _changeDownstreamPathTemplate = changeDownstreamPathTemplate; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); if (downstreamRoute.ClaimsToPath.Any()) { Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); var response = _changeDownstreamPathTemplate.ChangeDownstreamPath(downstreamRoute.ClaimsToPath, httpContext.User.Claims, downstreamRoute.DownstreamPathTemplate, templatePlaceholderNameAndValues); if (response.IsError) { Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; } } await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/DownstreamRouteHolder.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; namespace Ocelot.DownstreamRouteFinder; public class DownstreamRouteHolder { public DownstreamRouteHolder() { } public DownstreamRouteHolder(List templatePlaceholderNameAndValues, Route route) { TemplatePlaceholderNameAndValues = templatePlaceholderNameAndValues; Route = route; } public List TemplatePlaceholderNameAndValues { get; } public Route Route { get; } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/DiscoveryDownstreamRouteFinder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure.Extensions; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.Finder; public class DiscoveryDownstreamRouteFinder : IDownstreamRouteProvider { public const char Dot = '.'; public const char Slash = '/'; public const char Question = '?'; private readonly ConcurrentDictionary> _cache; private readonly IRouteKeyCreator _routeKeyCreator; private readonly IUpstreamHeaderTemplatePatternCreator _upstreamHeaderTemplatePatternCreator; public DiscoveryDownstreamRouteFinder( IRouteKeyCreator routeKeyCreator, IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) { _cache = new(); _routeKeyCreator = routeKeyCreator; _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; } public Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders) { var serviceName = GetServiceName(upstreamUrlPath, out var serviceNamespace); var downstreamPath = GetDownstreamPath(upstreamUrlPath); var dynamicRoute = configuration.Routes .Where(r => r.IsDynamic) // process dynamic routes only .SelectMany(r => r.DownstreamRoute) .FirstOrDefault(dr => dr.ServiceName == serviceName && (serviceNamespace.IsEmpty() || dr.ServiceNamespace == serviceNamespace)); var loadBalancerKey = dynamicRoute != null ? dynamicRoute.LoadBalancerKey : _routeKeyCreator.Create(serviceNamespace, serviceName, configuration.LoadBalancerOptions); if (_cache.TryGetValue(loadBalancerKey, out var downstreamRouteHolder)) { return downstreamRouteHolder; } // TODO: Could it be that the static route functionality was possibly lost here? -> StaticRoutesCreator.SetUpRoute -> _upstreamTemplatePatternCreator var upstreamPathTemplate = new UpstreamPathTemplateBuilder().WithOriginalValue(upstreamUrlPath).Build(); var upstreamHeaderTemplates = _upstreamHeaderTemplatePatternCreator.Create(upstreamHeaders, false); // ? discoveryDownstreamRoute.UpstreamHeaders var routeBuilder = new DownstreamRouteBuilder() .WithServiceName(serviceName) .WithServiceNamespace(serviceNamespace) .WithAuthenticationOptions(configuration.AuthenticationOptions) .WithCacheOptions(configuration.CacheOptions) .WithDownstreamHttpVersion(configuration.DownstreamHttpVersion) .WithDownstreamHttpVersionPolicy(configuration.DownstreamHttpVersionPolicy) .WithDownstreamPathTemplate(downstreamPath) .WithDownstreamScheme(configuration.DownstreamScheme) .WithHttpHandlerOptions(configuration.HttpHandlerOptions) .WithLoadBalancerKey(loadBalancerKey) .WithLoadBalancerOptions(configuration.LoadBalancerOptions) .WithMetadata(configuration.MetadataOptions) .WithQosOptions(configuration.QoSOptions) .WithRateLimitOptions(configuration.RateLimitOptions) .WithUpstreamHeaders(upstreamHeaderTemplates as Dictionary) .WithUpstreamPathTemplate(upstreamPathTemplate) .WithTimeout(configuration.Timeout); if (dynamicRoute != null) { // We are set to replace IInternalConfiguration global options with the current options from actual dynamic route routeBuilder .WithAuthenticationOptions(dynamicRoute.AuthenticationOptions) .WithCacheOptions(dynamicRoute.CacheOptions) .WithDownstreamHttpVersion(dynamicRoute.DownstreamHttpVersion) .WithDownstreamHttpVersionPolicy(dynamicRoute.DownstreamHttpVersionPolicy) .WithDownstreamScheme(dynamicRoute.DownstreamScheme) .WithHttpHandlerOptions(dynamicRoute.HttpHandlerOptions) .WithLoadBalancerKey(loadBalancerKey/*dynamicRoute.LoadBalancerKey*/) .WithLoadBalancerOptions(dynamicRoute.LoadBalancerOptions) .WithMetadata(dynamicRoute.MetadataOptions) .WithQosOptions(dynamicRoute.QosOptions) .WithRateLimitOptions(dynamicRoute.RateLimitOptions) .WithServiceName(serviceName/*dynamicRoute.ServiceName*/) .WithServiceNamespace(serviceNamespace/*dynamicRoute.ServiceNamespace*/) .WithTimeout(dynamicRoute.Timeout); } var downstreamRoute = routeBuilder.Build(); var route = new Route(true, downstreamRoute) // IsDynamic -> true { UpstreamHeaderTemplates = upstreamHeaderTemplates, UpstreamHost = upstreamHost, UpstreamHttpMethod = [new(upstreamHttpMethod.Trim())], UpstreamTemplatePattern = upstreamPathTemplate, }; downstreamRouteHolder = new OkResponse(new DownstreamRouteHolder(new List(), route)); _cache.AddOrUpdate(loadBalancerKey, downstreamRouteHolder, (x, y) => downstreamRouteHolder); return downstreamRouteHolder; } private static string GetDownstreamPath(string upstreamUrlPath) { int index = upstreamUrlPath.IndexOf(Slash, 1); return index != -1 ? upstreamUrlPath[index..] : Slash.ToString(); } /// Gets service name and its namespace of request URL. /// Note: A namespace and service name should be separated by a '.' (dot) character. /// Example: http://ocelot.net/namespace.service-name/path URL. /// The upstream path. /// Extracted namespace. /// A object. protected virtual string GetServiceName(string upstreamUrlPath, out string serviceNamespace) { var path = upstreamUrlPath.AsSpan(); int index = path[1..].IndexOf(Slash); var name = index == -1 ? path[1..] : path.Slice(1, index).TrimEnd(Slash); index = name.IndexOf(Dot); serviceNamespace = index == -1 ? string.Empty : name[..index].ToString(); var serviceName = index == -1 ? name : name[++index..]; return serviceName.ToString(); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteFinder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.Finder; public class DownstreamRouteFinder : IDownstreamRouteProvider { private readonly IUrlPathToUrlTemplateMatcher _urlMatcher; private readonly IPlaceholderNameAndValueFinder _pathPlaceholderFinder; private readonly IHeadersToHeaderTemplatesMatcher _headerMatcher; private readonly IHeaderPlaceholderNameAndValueFinder _headerPlaceholderFinder; public DownstreamRouteFinder( IUrlPathToUrlTemplateMatcher urlMatcher, IPlaceholderNameAndValueFinder pathPlaceholderFinder, IHeadersToHeaderTemplatesMatcher headerMatcher, IHeaderPlaceholderNameAndValueFinder headerPlaceholderFinder) { _urlMatcher = urlMatcher; _pathPlaceholderFinder = pathPlaceholderFinder; _headerMatcher = headerMatcher; _headerPlaceholderFinder = headerPlaceholderFinder; } public Response Get(string upstreamUrlPath, string upstreamQueryString, string httpMethod, IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders) { var downstreamRoutes = new List(); var applicableRoutes = configuration.Routes .Where(r => !r.IsDynamic && RouteIsApplicableToThisRequest(r, httpMethod, upstreamHost)) // process static routes only .OrderByDescending(x => x.UpstreamTemplatePattern.Priority); foreach (var route in applicableRoutes) { var urlMatch = _urlMatcher.Match(upstreamUrlPath, upstreamQueryString, route.UpstreamTemplatePattern); var headersMatch = _headerMatcher.Match(upstreamHeaders, route.UpstreamHeaderTemplates); if (urlMatch.Match && headersMatch) { downstreamRoutes.Add(GetPlaceholderNamesAndValues(upstreamUrlPath, upstreamQueryString, route, upstreamHeaders)); } } if (downstreamRoutes.Count != 0) { var notNullOption = downstreamRoutes.FirstOrDefault(x => !string.IsNullOrEmpty(x.Route.UpstreamHost)); var nullOption = downstreamRoutes.FirstOrDefault(x => string.IsNullOrEmpty(x.Route.UpstreamHost)); return new OkResponse(notNullOption ?? nullOption); } return new ErrorResponse(new UnableToFindDownstreamRouteError(upstreamUrlPath, httpMethod)); } private static bool RouteIsApplicableToThisRequest(Route route, string httpMethod, string upstreamHost) { var method = new HttpMethod(httpMethod.Trim()); return (route.UpstreamHttpMethod.Count == 0 || route.UpstreamHttpMethod.Contains(method)) && (string.IsNullOrEmpty(route.UpstreamHost) || route.UpstreamHost == upstreamHost); } private DownstreamRouteHolder GetPlaceholderNamesAndValues(string path, string query, Route route, IHeaderDictionary upstreamHeaders) { var templatePlaceholderNameAndValues = _pathPlaceholderFinder .Find(path, query, route.UpstreamTemplatePattern.OriginalValue) .Data; var headerPlaceholders = _headerPlaceholderFinder.Find(upstreamHeaders, route.UpstreamHeaderTemplates); templatePlaceholderNameAndValues.AddRange(headerPlaceholders); return new DownstreamRouteHolder(templatePlaceholderNameAndValues, route); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; namespace Ocelot.DownstreamRouteFinder.Finder; public class DownstreamRouteProviderFactory : IDownstreamRouteProviderFactory { private readonly Dictionary _providers; // TODO We need to use a HashSet here for quicker lookups private readonly IOcelotLogger _logger; public DownstreamRouteProviderFactory(IServiceProvider provider, IOcelotLoggerFactory factory) { _logger = factory.CreateLogger(); _providers = provider.GetServices().ToDictionary(x => x.GetType().Name); } public IDownstreamRouteProvider Get(IInternalConfiguration config) { //todo - this is a bit hacky we are saying there are no routes or there are routes but none of them have //an upstream path template which means they are dyanmic and service discovery is on... if ((config.Routes.Length == 0 || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) { _logger.LogInformation($"Selected {nameof(DiscoveryDownstreamRouteFinder)} as {nameof(IDownstreamRouteProvider)} for this request"); return _providers[nameof(DiscoveryDownstreamRouteFinder)]; } return _providers[nameof(DownstreamRouteFinder)]; } private static bool IsServiceDiscovery(ServiceProviderConfiguration config) { return !string.IsNullOrEmpty(config?.Host) && config?.Port > 0 && !string.IsNullOrEmpty(config?.Type); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProvider.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.Finder; public interface IDownstreamRouteProvider { Response Get(string upstreamUrlPath, string upstreamQueryString, string upstreamHttpMethod, IInternalConfiguration configuration, string upstreamHost, IHeaderDictionary upstreamHeaders); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/IDownstreamRouteProviderFactory.cs ================================================ using Ocelot.Configuration; namespace Ocelot.DownstreamRouteFinder.Finder; public interface IDownstreamRouteProviderFactory { IDownstreamRouteProvider Get(IInternalConfiguration config); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Finder/UnableToFindDownstreamRouteError.cs ================================================ using Ocelot.Errors; using Status = System.Net.HttpStatusCode; namespace Ocelot.DownstreamRouteFinder.Finder; public class UnableToFindDownstreamRouteError : Error { public UnableToFindDownstreamRouteError(string path, string httpVerb) : base($"Failed to match route configuration for upstream: {httpVerb} {path}", OcelotErrorCode.UnableToFindDownstreamRouteError, (int)Status.NotFound) { } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; public class HeaderPlaceholderNameAndValueFinder : IHeaderPlaceholderNameAndValueFinder { public IList Find(IHeaderDictionary upstreamHeaders, IDictionary templateHeaders) { var result = new List(); foreach (var templateHeader in templateHeaders) { var upstreamHeader = upstreamHeaders[templateHeader.Key]; var matches = templateHeader.Value.Pattern.Matches(upstreamHeader); var placeholders = matches .SelectMany(g => g.Groups as IEnumerable) .Where(g => g.Name != "0") .Select(g => new PlaceholderNameAndValue(string.Concat('{', g.Name, '}'), g.Value)); result.AddRange(placeholders); } return result; } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcher.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; public class HeadersToHeaderTemplatesMatcher : IHeadersToHeaderTemplatesMatcher { public bool Match(IHeaderDictionary upstreamHeaders, IDictionary routeHeaders) { bool IsMatching(KeyValuePair h) { return upstreamHeaders.TryGetValue(h.Key, out StringValues header) && routeHeaders[h.Key].Pattern.IsMatch(header); } return routeHeaders == null || upstreamHeaders != null && routeHeaders.All(IsMatching); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeaderPlaceholderNameAndValueFinder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; /// /// Ocelot feature: Routing based on request header. /// public interface IHeaderPlaceholderNameAndValueFinder { IList Find(IHeaderDictionary upstreamHeaders, IDictionary templateHeaders); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/HeaderMatcher/IHeadersToHeaderTemplatesMatcher.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.HeaderMatcher; /// /// Ocelot feature: Routing based on request header. /// public interface IHeadersToHeaderTemplatesMatcher { bool Match(IHeaderDictionary upstreamHeaders, IDictionary routeHeaders); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.DownstreamRouteFinder.Middleware; public class DownstreamRouteFinderMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IDownstreamRouteProviderFactory _factory; public DownstreamRouteFinderMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamRouteProviderFactory downstreamRouteFinder) : base(loggerFactory.CreateLogger()) { _next = next; _factory = downstreamRouteFinder; } public async Task Invoke(HttpContext httpContext) { var upstreamUrlPath = httpContext.Request.Path.ToString(); var upstreamQueryString = httpContext.Request.QueryString.ToString(); var internalConfiguration = httpContext.Items.IInternalConfiguration(); var hostHeader = httpContext.Request.Headers.Host.ToString(); var upstreamHost = hostHeader.Contains(':') ? hostHeader.Split(':')[0] : hostHeader; var upstreamHeaders = httpContext.Request.Headers; Logger.LogDebug(() => $"Upstream URL path: {upstreamUrlPath}"); var provider = _factory.Get(internalConfiguration); var response = provider.Get(upstreamUrlPath, upstreamQueryString, httpContext.Request.Method, internalConfiguration, upstreamHost, upstreamHeaders); if (response.IsError) { Logger.LogWarning(() => $"{MiddlewareName} setting pipeline errors because {provider.GetType().Name} returned the following ->{response.Errors.ToErrorString(true)}"); httpContext.Items.UpsertErrors(response.Errors); return; } Logger.LogDebug(() => $"Downstream templates: {string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}"); // why set both of these on HttpContext httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); httpContext.Items.UpsertDownstreamRoute(response.Data); await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/IPlaceholderNameAndValueFinder.cs ================================================ using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.UrlMatcher; public interface IPlaceholderNameAndValueFinder { Response> Find(string path, string query, string pathTemplate); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/IUrlPathToUrlTemplateMatcher.cs ================================================ using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.UrlMatcher; public interface IUrlPathToUrlTemplateMatcher { UrlMatch Match(string upstreamUrlPath, string upstreamQueryString, UpstreamPathTemplate pathTemplate); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValue.cs ================================================ using Ocelot.Infrastructure; namespace Ocelot.DownstreamRouteFinder.UrlMatcher; public class PlaceholderNameAndValue { private const char OpeningBrace = Placeholders.OpeningBrace; private const char ClosingBrace = Placeholders.ClosingBrace; public PlaceholderNameAndValue(string name, string value) { Name = name; Value = value; } public string Name { get; } public string Value { get; } public string Key { get => Name.Trim(OpeningBrace, ClosingBrace); } public override string ToString() => $"[{Name}={Value}]"; } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcher.cs ================================================ using Ocelot.Values; namespace Ocelot.DownstreamRouteFinder.UrlMatcher; public class RegExUrlMatcher : IUrlPathToUrlTemplateMatcher { public UrlMatch Match(string upstreamUrlPath, string upstreamQueryString, UpstreamPathTemplate pathTemplate) => !pathTemplate.ContainsQueryString ? new(pathTemplate.Pattern.IsMatch(upstreamUrlPath)) : new(pathTemplate.Pattern.IsMatch($"{upstreamUrlPath}{upstreamQueryString}")); } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlMatch.cs ================================================ namespace Ocelot.DownstreamRouteFinder.UrlMatcher; public class UrlMatch { public UrlMatch(bool match) { Match = match; } public bool Match { get; } } ================================================ FILE: src/Ocelot/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinder.cs ================================================ using Ocelot.Infrastructure; using Ocelot.Responses; namespace Ocelot.DownstreamRouteFinder.UrlMatcher; /// The finder locates all occurrences of placeholders' names and values within URL paths. /// This is the default implementation of the interface. /// public partial class UrlPathPlaceholderNameAndValueFinder : IPlaceholderNameAndValueFinder { private const char LeftBrace = '{'; private const char RightBrace = '}'; /// Finds the placeholders in the request path and query and returns their matching values. /// We might encounter the following scenarios: /// /// The path template contains a Catch-All query parameter. If so, we return the Catch-All placeholder with an empty value. /// The path template contains a Catch-All path parameter. If so, we return the Catch-All placeholder with an empty value. /// The path template contains placeholders. We return the placeholders with their matching values. /// /// /// /// The request path. /// The query parameters. /// The request path template. /// A object, where T is : the list of the placeholders with their matching values. public Response> Find(string path, string query, string pathTemplate) { (bool isCatchAllQuery, string catchAllQueryPlaceholder) = IsCatchAllQuery(pathTemplate); if (isCatchAllQuery) { return new OkResponse>(new List { new($"{LeftBrace}{catchAllQueryPlaceholder}{RightBrace}", string.Empty), }); } // Find matching groups from path and query var placeholders = FindGroups(path, query, pathTemplate) .Select(g => new PlaceholderNameAndValue($"{LeftBrace}{g.Name}{RightBrace}", g.Value)) .ToList(); return new OkResponse>(placeholders); } private const int PlaceholdersMilliseconds = 1000; [GeneratedRegex(@"\{(.*?)\}", RegexOptions.None, PlaceholdersMilliseconds)] private static partial Regex RegexPlaceholders(); /// Finds the placeholders in the request path and query. /// We use a pattern to match the placeholders in the path template. /// We have two slight optimizations: /// /// First, we skip the query if it is not present in the path template. /// Second, we append a trailing slash to the path if it is a Catch-All path. /// /// /// /// The request path. /// The query parameters. /// The path template. /// A object (T is ): the matching groups. private static List FindGroups(string path, string query, string template) { template = EscapeExceptBraces(template); var regexPattern = GenerateRegexPattern(template); var testedPath = ShouldSkipQuery(query, template) ? path : $"{path}{query}"; var match = Regex.Match(testedPath, regexPattern); var foundGroups = match.Groups.Cast().Skip(1).ToList(); if (foundGroups.Count > 0 || !IsCatchAllPath(template)) { return foundGroups; } // Append a trailing slash to the path if it is a catch-all path match = Regex.Match($"{testedPath}/", regexPattern); return match.Groups.Cast().Skip(1).ToList(); } /// /// The placeholders that are not placed at the end of the template are delimited by forward slashes, only the last one, the catch-all can match more segments. /// /// The escaped path template. /// The pattern for values replacement. private static string GenerateRegexPattern(string escapedTemplate) { // First we count the matches var placeHoldersCountMatch = RegexPlaceholders().Matches(escapedTemplate); int index = 0, placeHoldersCount = placeHoldersCountMatch.Count; // We know that the replace process will be started from the beginning of the url, // so we can use a simple counter to determine the last placeholder string MatchEvaluator(Match match) { var groupName = match.Groups[1].Value; index++; return index == placeHoldersCount ? $"(?<{groupName}>[^&]*)" : $"(?<{groupName}>[^/|&]*)"; } return $@"^{RegexPlaceholders().Replace(escapedTemplate, MatchEvaluator)}"; } private const int CatchAllQueryMilliseconds = 300; [GeneratedRegex(@"^[^{{}}]*\?\{(.*?)\}$", RegexOptions.None, CatchAllQueryMilliseconds)] private static partial Regex RegexCatchAllQuery(); /// Checks if the path template contains a Catch-All query parameter. /// It means that the path template ends with a question mark and a placeholder. /// And no other placeholders are present in the path template. /// /// The path template. /// if it matches and the found placeholder. private static (bool IsMatch, string Placeholder) IsCatchAllQuery(string template) { var catchAllMatch = RegexCatchAllQuery().Match(template); return (catchAllMatch.Success, catchAllMatch.Success ? catchAllMatch.Groups[1].Value : string.Empty); } private const int CatchAllPathMilliseconds = 300; [GeneratedRegex(@"^[^{{}}]*\{(.*?)\}/?$", RegexOptions.None, CatchAllPathMilliseconds)] private static partial Regex RegexCatchAllPath(); /// Check if the path template contains a Catch-All path parameter. /// It means that the path template ends with a placeholder and no other placeholders are present in the path template, without a question mark (query parameters). /// /// The path template. /// if it matches. private static bool IsCatchAllPath(string template) => RegexCatchAllPath().IsMatch(template) && !template.Contains(@"\?"); /// Checks if the query should be skipped. /// It should be skipped if it is not present in the path template. /// Since the template is escaped, looking for \? not only ?. /// /// The query string. /// The path template. /// if query should be skipped. private static bool ShouldSkipQuery(string query, string template) => !string.IsNullOrEmpty(query) && !template.Contains(@"\?"); /// Escapes all characters except braces, eg { and }. /// The input string. /// The formatted . private static string EscapeExceptBraces(string input) { if (string.IsNullOrEmpty(input)) { return string.Empty; } StringBuilder escaped = new(); foreach (char c in input.AsSpan()) { if (c is LeftBrace or RightBrace) { escaped.Append(c); } else { escaped.Append(Regex.Escape(c.ToString())); } } // Here we are not interested in the path itself, only the placeholders. // Path validation is not part of this class, therefore allowing case-insensitive // matching. return $"^(?i){escaped}"; } } ================================================ FILE: src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator; /// /// TODO Move this service to the middleware as a protected virtual method. Having a separate interface is absolutely useless. /// public class DownstreamPathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer { public DownstreamPath Replace(string downstreamPathTemplate, List urlPathPlaceholderNameAndValues) { var downstreamPath = new StringBuilder(downstreamPathTemplate); foreach (var placeholderVariableAndValue in urlPathPlaceholderNameAndValues) { downstreamPath.Replace(placeholderVariableAndValue.Name, placeholderVariableAndValue.Value); } return new(downstreamPath.ToString()); } } ================================================ FILE: src/Ocelot/DownstreamUrlCreator/DownstreamUrlCreatorMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.WebUtilities; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using System.Web; namespace Ocelot.DownstreamUrlCreator; public class DownstreamUrlCreatorMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IDownstreamPathPlaceholderReplacer _replacer; private const char Ampersand = '&'; private const char QuestionMark = '?'; protected const char Slash = '/'; public DownstreamUrlCreatorMiddleware( RequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamPathPlaceholderReplacer replacer) : base(loggerFactory.CreateLogger()) { _next = next; _replacer = replacer; } public async Task Invoke(HttpContext context) { var downstreamRoute = context.Items.DownstreamRoute(); var placeholders = context.Items.TemplatePlaceholderNameAndValues(); var downstreamPath = _replacer.Replace(downstreamRoute.DownstreamPathTemplate.Value, placeholders); if (downstreamPath.Value.IsEmpty()) { throw new NotSupportedException($"{_replacer.GetType().Name} returned an empty {nameof(DownstreamPath)} for the route {downstreamRoute.Name()}."); } var dsPath = downstreamPath.Value; var downstreamRequest = context.Items.DownstreamRequest(); var upstreamPath = downstreamRequest.AbsolutePath; if (dsPath.EndsWith(Slash) && !upstreamPath.EndsWith(Slash)) { dsPath = dsPath.TrimEnd(Slash); downstreamPath = new DownstreamPath(dsPath); } if (!string.IsNullOrEmpty(downstreamRoute.DownstreamScheme)) { // TODO Make sure this works, hopefully there is a test ;E context.Items.DownstreamRequest().Scheme = downstreamRoute.DownstreamScheme; } var internalConfiguration = context.Items.IInternalConfiguration(); if (ServiceFabricRequest(internalConfiguration, downstreamRoute)) { var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, placeholders, downstreamPath); // TODO Check this works again hope there is a test.. downstreamRequest.AbsolutePath = path; downstreamRequest.Query = query; } else { if (dsPath.Contains(QuestionMark)) { downstreamRequest.AbsolutePath = GetPath(dsPath).ToString(); var newQuery = GetQueryString(dsPath).ToString(); downstreamRequest.Query = string.IsNullOrEmpty(downstreamRequest.Query) ? newQuery : MergeQueryStringsWithoutDuplicateValues(downstreamRequest.Query, newQuery, placeholders); } else { downstreamRequest.AbsolutePath = dsPath; downstreamRequest.Query = RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, placeholders); } } Logger.LogDebug(() => $"Downstream URL: {downstreamRequest}"); await _next.Invoke(context); } /// /// Merging of Query Parameters is part of /// the Query Placeholders feature. /// /// A object. protected static string MergeQueryStringsWithoutDuplicateValues(string queryString, string newQueryString, List placeholders) { newQueryString = newQueryString.Replace(QuestionMark, Ampersand); var queries = HttpUtility.ParseQueryString(queryString); var newQueries = HttpUtility.ParseQueryString(newQueryString); // Remove old replaced query parameters var placeholderKeys = new HashSet(placeholders.Select(p => p.Key)); foreach (var queryKey in queries.AllKeys.Where(placeholderKeys.Contains)) { queries.Remove(queryKey); } var parameters = newQueries.AllKeys .Where(key => key.IsNotEmpty()) .ToDictionary(key => key, key => newQueries[key]); _ = queries.AllKeys .Where(key => key.IsNotEmpty() && !parameters.ContainsKey(key)) .All(key => parameters.TryAdd(key, queries[key])); return QueryHelpers.AddQueryString(string.Empty, parameters); } /// /// Feature 467: Added support for query string parameters in upstream path template. /// /// A object without wanted parameters. protected static string RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest request, List templatePlaceholders) { if (templatePlaceholders.Count == 0 || request.Query.IsEmpty()) { return request.Query; } var query = QueryHelpers.ParseQuery(request.Query); foreach (var placeholder in templatePlaceholders.Where(p => query.ContainsKey(p.Key))) { query.Remove(placeholder.Key); } return QueryHelpers.AddQueryString(string.Empty, query); } protected static ReadOnlySpan GetPath(ReadOnlySpan downstreamPath) { int length = downstreamPath.IndexOf(QuestionMark); return length >= 0 ? downstreamPath[..length] : downstreamPath; } protected static ReadOnlySpan GetQueryString(ReadOnlySpan downstreamPath) { int startIndex = downstreamPath.IndexOf(QuestionMark); return startIndex >= 0 ? downstreamPath[startIndex..] : ReadOnlySpan.Empty; } protected (string Path, string Query) CreateServiceFabricUri(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute, List templatePlaceholderNameAndValues, DownstreamPath dsPath) { var query = downstreamRequest.Query; var serviceName = _replacer.Replace(downstreamRoute.ServiceName, templatePlaceholderNameAndValues); var pathTemplate = $"/{serviceName.Value}{dsPath.Value}"; return (pathTemplate, query); } protected static bool ServiceFabricRequest(IInternalConfiguration config, DownstreamRoute route) => ServiceFabricServiceDiscoveryProvider.Type.Equals(config.ServiceProviderConfiguration.Type, StringComparison.OrdinalIgnoreCase) && route.UseServiceDiscovery; } ================================================ FILE: src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.DownstreamUrlCreator; public interface IDownstreamPathPlaceholderReplacer { DownstreamPath Replace(string downstreamPathTemplate, List urlPathPlaceholderNameAndValues); } ================================================ FILE: src/Ocelot/Errors/Error.cs ================================================ namespace Ocelot.Errors; public abstract class Error { protected Error(string message, OcelotErrorCode code, int httpStatusCode) { HttpStatusCode = httpStatusCode; Message = message; Code = code; } public string Message { get; } public OcelotErrorCode Code { get; } public int HttpStatusCode { get; } public override string ToString() => $"{Code}: {Message}"; } ================================================ FILE: src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Infrastructure.Extensions; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Errors.Middleware; /// /// Catches all unhandled exceptions thrown by middleware, logs and returns a 500. /// public class ExceptionHandlerMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IRequestScopedDataRepository _repo; public ExceptionHandlerMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository repo) : base(loggerFactory.CreateLogger()) { _next = next; _repo = repo; } public async Task Invoke(HttpContext context) { try { context.RequestAborted.ThrowIfCancellationRequested(); var configuration = context.Items.IInternalConfiguration(); TrySetGlobalRequestId(context, configuration); Logger.LogDebug("Ocelot pipeline started"); await _next.Invoke(context); } catch (OperationCanceledException e) when (context.RequestAborted.IsCancellationRequested) { Logger.LogDebug("Operation canceled"); Logger.LogWarning(() => CreateMessage(context, e, true)); if (!context.Response.HasStarted) { context.Response.StatusCode = StatusCodes.Status499ClientClosedRequest; // custom Ocelot code } } catch (Exception e) { Logger.LogDebug("Error calling middleware"); Logger.LogError(() => CreateMessage(context, e), e); if (!context.Response.HasStarted) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } } finally { Logger.LogDebug("Ocelot pipeline finished"); } } private void TrySetGlobalRequestId(HttpContext context, IInternalConfiguration configuration) { var key = configuration.RequestId; if (!string.IsNullOrEmpty(key) && context.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) { context.TraceIdentifier = upstreamRequestIds.First(); } _repo.Add(nameof(IInternalConfiguration.RequestId), context.TraceIdentifier); } private static string CreateMessage(HttpContext context, Exception e, bool includeException = false) { var original = e; var builder = new StringBuilder() .AppendLine($"{e.GetType().Name} caught in global error handler!"); int total = 0; while (e.InnerException != null) { builder.AppendLine(e.InnerException.ToString()); e = e.InnerException; total++; } builder.Append($"DONE reporting of a total {total} inner exception{total.Plural()} for request {context.TraceIdentifier} of the original {original.GetType().Name} below ->"); if (includeException) { builder.Append(Environment.NewLine + original.ToString()); } return builder.ToString(); } } ================================================ FILE: src/Ocelot/Errors/OcelotErrorCode.cs ================================================ namespace Ocelot.Errors; public enum OcelotErrorCode { UnauthenticatedError = 0, UnknownError = 1, DownstreampathTemplateAlreadyUsedError = 2, UnableToFindDownstreamRouteError = 3, CannotAddDataError = 4, CannotFindDataError = 5, UnableToCompleteRequestError = 6, UnableToCreateAuthenticationHandlerError = 7, UnsupportedAuthenticationProviderError = 8, CannotFindClaimError = 9, ParsingConfigurationHeaderError = 10, NoInstructionsError = 11, InstructionNotForClaimsError = 12, UnauthorizedError = 13, ClaimValueNotAuthorizedError = 14, ScopeNotAuthorizedError = 15, UserDoesNotHaveClaimError = 16, DownstreamPathTemplateContainsSchemeError = 17, DownstreamPathNullOrEmptyError = 18, DownstreamSchemeNullOrEmptyError = 19, DownstreamHostNullOrEmptyError = 20, ServicesAreNullError = 21, ServicesAreEmptyError = 22, UnableToFindServiceDiscoveryProviderError = 23, UnableToFindLoadBalancerError = 24, RequestTimedOutError = 25, UnableToFindQoSProviderError = 26, UnmappableRequestError = 27, RateLimitOptionsError = 28, PathTemplateDoesntStartWithForwardSlash = 29, FileValidationFailedError = 30, UnableToFindDelegatingHandlerProviderError = 31, CouldNotFindPlaceholderError = 32, CouldNotFindAggregatorError = 33, CannotAddPlaceholderError = 34, CannotRemovePlaceholderError = 35, QuotaExceededError = 36, RequestCanceled = 37, ConnectionToDownstreamServiceError = 38, CouldNotFindLoadBalancerCreator = 39, ErrorInvokingLoadBalancerCreator = 40, PayloadTooLargeError = 41, } ================================================ FILE: src/Ocelot/Errors/RequestTimedOutError.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.Errors; public class RequestTimedOutError : Error { public RequestTimedOutError(Exception exception) : base($"Timeout making http request, exception: {exception}", OcelotErrorCode.RequestTimedOutError, StatusCodes.Status503ServiceUnavailable) { } } ================================================ FILE: src/Ocelot/Headers/AddHeadersToRequest.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Logging; using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Headers; public class AddHeadersToRequest : IAddHeadersToRequest { private readonly IClaimsParser _claimsParser; private readonly IPlaceholders _placeholders; private readonly IOcelotLogger _logger; public AddHeadersToRequest(IClaimsParser claimsParser, IPlaceholders placeholders, IOcelotLoggerFactory factory) { _logger = factory.CreateLogger(); _claimsParser = claimsParser; _placeholders = placeholders; } public Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest) { foreach (var config in claimsToThings) { var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index); if (value.IsError) { return new ErrorResponse(value.Errors); } var exists = downstreamRequest.Headers.FirstOrDefault(x => x.Key == config.ExistingKey); if (!string.IsNullOrEmpty(exists.Key)) { downstreamRequest.Headers.Remove(exists.Key); } downstreamRequest.Headers.Add(config.ExistingKey, value.Data); } return new OkResponse(); } public void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpContext context) { var requestHeader = context.Request.Headers; foreach (var header in headers) { if (requestHeader.ContainsKey(header.Key)) { requestHeader.Remove(header.Key); } if (header.Value.StartsWith('{') && header.Value.EndsWith("}")) { var value = _placeholders.Get(header.Value); if (value.IsError) { _logger.LogWarning(() => $"Unable to add header to response {header.Key}: {header.Value}"); continue; } requestHeader.Append(header.Key, new StringValues(value.Data)); } else { requestHeader.Append(header.Key, header.Value); } } } } ================================================ FILE: src/Ocelot/Headers/AddHeadersToResponse.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Headers; public class AddHeadersToResponse : IAddHeadersToResponse { private readonly IPlaceholders _placeholders; private readonly IOcelotLogger _logger; public AddHeadersToResponse(IPlaceholders placeholders, IOcelotLoggerFactory factory) { _logger = factory.CreateLogger(); _placeholders = placeholders; } public void Add(List addHeaders, DownstreamResponse response) { foreach (var add in addHeaders) { if (add.Value.StartsWith('{') && add.Value.EndsWith('}')) { var value = _placeholders.Get(add.Value); if (value.IsError) { _logger.LogWarning(() => $"Unable to add header to response {add.Key}: {add.Value}"); continue; } response.Headers.Add(new Header(add.Key, new List { value.Data })); } else { response.Headers.Add(new Header(add.Key, new List { add.Value })); } } } } ================================================ FILE: src/Ocelot/Headers/HttpContextRequestHeaderReplacer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Headers; public class HttpContextRequestHeaderReplacer : IHttpContextRequestHeaderReplacer { public Response Replace(HttpContext context, List fAndRs) { foreach (var f in fAndRs) { if (context.Request.Headers.TryGetValue(f.Key, out var values)) { var replaced = values[f.Index].Replace(f.Find, f.Replace); context.Request.Headers.Remove(f.Key); context.Request.Headers.Append(f.Key, replaced); } } return new OkResponse(); } } ================================================ FILE: src/Ocelot/Headers/HttpResponseHeaderReplacer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Headers; public class HttpResponseHeaderReplacer : IHttpResponseHeaderReplacer { private readonly IPlaceholders _placeholders; public HttpResponseHeaderReplacer(IPlaceholders placeholders) { _placeholders = placeholders; } public Response Replace(HttpContext httpContext, List fAndRs) { var response = httpContext.Items.DownstreamResponse(); var request = httpContext.Items.DownstreamRequest(); foreach (var f in fAndRs) { var dict = response.Headers.ToDictionary(x => x.Key); //if the response headers contain a matching find and replace if (dict.TryGetValue(f.Key, out var values)) { //check to see if it is a placeholder in the find... var placeholderValue = _placeholders.Get(f.Find, request); if (!placeholderValue.IsError) { //if it is we need to get the value of the placeholder var replaced = values.Values.ToList()[f.Index].Replace(placeholderValue.Data, f.Replace.LastCharAsForwardSlash()); response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); response.Headers.Add(new Header(f.Key, new List { replaced })); } else { var replaced = values.Values.ToList()[f.Index].Replace(f.Find, f.Replace); response.Headers.Remove(response.Headers.First(item => item.Key == f.Key)); response.Headers.Add(new Header(f.Key, new List { replaced })); } } } return new OkResponse(); } } ================================================ FILE: src/Ocelot/Headers/IAddHeadersToRequest.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Headers; public interface IAddHeadersToRequest { Response SetHeadersOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest); void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpContext context); } ================================================ FILE: src/Ocelot/Headers/IAddHeadersToResponse.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Middleware; namespace Ocelot.Headers; public interface IAddHeadersToResponse { void Add(List addHeaders, DownstreamResponse response); } ================================================ FILE: src/Ocelot/Headers/IHttpContextRequestHeaderReplacer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Headers; public interface IHttpContextRequestHeaderReplacer { Response Replace(HttpContext context, List fAndRs); } ================================================ FILE: src/Ocelot/Headers/IHttpResponseHeaderReplacer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Headers; public interface IHttpResponseHeaderReplacer { public Response Replace(HttpContext httpContext, List fAndRs); } ================================================ FILE: src/Ocelot/Headers/IRemoveOutputHeaders.cs ================================================ using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Headers; public interface IRemoveOutputHeaders { Response Remove(List
headers); } ================================================ FILE: src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Headers.Middleware; public class ClaimsToHeadersMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IAddHeadersToRequest _addHeadersToRequest; public ClaimsToHeadersMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IAddHeadersToRequest addHeadersToRequest) : base(loggerFactory.CreateLogger()) { _next = next; _addHeadersToRequest = addHeadersToRequest; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); if (downstreamRoute.ClaimsToHeaders.Any()) { Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); var downstreamRequest = httpContext.Items.DownstreamRequest(); var response = _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.ClaimsToHeaders, httpContext.User.Claims, downstreamRequest); if (response.IsError) { Logger.LogWarning("Error setting headers on context, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; } Logger.LogInformation("headers have been set on context"); } await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/Headers/Middleware/HttpHeadersTransformationMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Headers.Middleware; public class HttpHeadersTransformationMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IHttpContextRequestHeaderReplacer _preReplacer; private readonly IHttpResponseHeaderReplacer _postReplacer; private readonly IAddHeadersToResponse _addHeadersToResponse; private readonly IAddHeadersToRequest _addHeadersToRequest; public HttpHeadersTransformationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpContextRequestHeaderReplacer preReplacer, IHttpResponseHeaderReplacer postReplacer, IAddHeadersToResponse addHeadersToResponse, IAddHeadersToRequest addHeadersToRequest ) : base(loggerFactory.CreateLogger()) { _addHeadersToResponse = addHeadersToResponse; _addHeadersToRequest = addHeadersToRequest; _next = next; _postReplacer = postReplacer; _preReplacer = preReplacer; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); var preFAndRs = downstreamRoute.UpstreamHeadersFindAndReplace; //todo - this should be on httprequestmessage not httpcontext? _preReplacer.Replace(httpContext, preFAndRs); _addHeadersToRequest.SetHeadersOnDownstreamRequest(downstreamRoute.AddHeadersToUpstream, httpContext); await _next.Invoke(httpContext); // todo check errors is ok //todo put this check on the base class? if (httpContext.Items.Errors().Count > 0) { return; } var postFAndRs = downstreamRoute.DownstreamHeadersFindAndReplace; _postReplacer.Replace(httpContext, postFAndRs); var downstreamResponse = httpContext.Items.DownstreamResponse(); _addHeadersToResponse.Add(downstreamRoute.AddHeadersToDownstream, downstreamResponse); } } ================================================ FILE: src/Ocelot/Headers/RemoveOutputHeaders.cs ================================================ using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Headers; public class RemoveOutputHeaders : IRemoveOutputHeaders { /// /// 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 /// returning the response in this manner. /// private readonly string[] _unsupportedRequestHeaders = { "Transfer-Encoding", }; public Response Remove(List
headers) { headers.RemoveAll(x => _unsupportedRequestHeaders.Contains(x.Key)); return new OkResponse(); } } ================================================ FILE: src/Ocelot/Infrastructure/CannotAddPlaceholderError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure; public class CannotAddPlaceholderError : Error { public CannotAddPlaceholderError(string message) : base(message, OcelotErrorCode.CannotAddPlaceholderError, 404) { } } ================================================ FILE: src/Ocelot/Infrastructure/CannotRemovePlaceholderError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure; public class CannotRemovePlaceholderError : Error { public CannotRemovePlaceholderError(string message) : base(message, OcelotErrorCode.CannotRemovePlaceholderError, 404) { } } ================================================ FILE: src/Ocelot/Infrastructure/Claims/CannotFindClaimError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure.Claims; public class CannotFindClaimError : Error { public CannotFindClaimError(string message) : base(message, OcelotErrorCode.CannotFindClaimError, 403) { } } ================================================ FILE: src/Ocelot/Infrastructure/Claims/ClaimsParser.cs ================================================ using Microsoft.Extensions.Primitives; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Infrastructure.Claims; public class ClaimsParser : IClaimsParser { public Response GetValue(IEnumerable claims, string key, string delimiter, int index) { var claimResponse = GetValue(claims, key); if (claimResponse.IsError) { return claimResponse; } if (string.IsNullOrEmpty(delimiter)) { return claimResponse; } var splits = claimResponse.Data.Split(delimiter.ToCharArray()); if (splits.Length <= index || index < 0) { return new ErrorResponse(new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}")); } var value = splits[index]; return new OkResponse(value); } public Response> GetValuesByClaimType(IEnumerable claims, string claimType) { var values = claims .Where(x => x.Type == claimType) // Case sensitive or insensitive? That's the question! .Select(x => x.Value) .ToList(); return new OkResponse>(values); } private static Response GetValue(IEnumerable claims, string key) { var claimValues = claims.Where(c => c.Type == key).Select(c => c.Value).ToArray(); if (claimValues.Length > 0) { return new OkResponse(new StringValues(claimValues).ToString()); } return new ErrorResponse(new CannotFindClaimError($"Cannot find claim for key: {key}")); } } ================================================ FILE: src/Ocelot/Infrastructure/Claims/IClaimsParser.cs ================================================ using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.Infrastructure.Claims; public interface IClaimsParser { Response GetValue(IEnumerable claims, string key, string delimiter, int index); Response> GetValuesByClaimType(IEnumerable claims, string claimType); } ================================================ FILE: src/Ocelot/Infrastructure/ConfigAwarePlaceholders.cs ================================================ using Microsoft.Extensions.Configuration; using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Infrastructure; /// /// The configuration related implementation of the interface. /// public partial class ConfigAwarePlaceholders : IPlaceholders { private readonly IConfiguration _configuration; private readonly IPlaceholders _placeholders; public ConfigAwarePlaceholders(IConfiguration configuration, IPlaceholders placeholders) { _configuration = configuration; _placeholders = placeholders; } public Response Get(string key) { var placeholderResponse = _placeholders.Get(key); if (!placeholderResponse.IsError) { return placeholderResponse; } return GetFromConfig(CleanKey(key)); } public Response Get(string key, DownstreamRequest request) { var placeholderResponse = _placeholders.Get(key, request); if (!placeholderResponse.IsError) { return placeholderResponse; } return GetFromConfig(CleanKey(key)); } public Response Add(string key, Func> func) => _placeholders.Add(key, func); public Response Remove(string key) => _placeholders.Remove(key); [GeneratedRegex(@"[{}]", RegexOptions.None, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex Regex(); private static string CleanKey(string key) => Regex().Replace(key, string.Empty); private Response GetFromConfig(string key) { var valueFromConfig = _configuration[key]; return valueFromConfig == null ? new ErrorResponse(new CouldNotFindPlaceholderError(key)) : new OkResponse(valueFromConfig); } } ================================================ FILE: src/Ocelot/Infrastructure/CouldNotFindPlaceholderError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure; public class CouldNotFindPlaceholderError : Error { public CouldNotFindPlaceholderError(string placeholder) : base($"Unable to find placeholder called {placeholder}", OcelotErrorCode.CouldNotFindPlaceholderError, 404) { } } ================================================ FILE: src/Ocelot/Infrastructure/DelayedMessage.cs ================================================ namespace Ocelot.Infrastructure; internal class DelayedMessage { public DelayedMessage(T message, int delay) { Delay = delay; Message = message; } public T Message { get; set; } public int Delay { get; set; } } ================================================ FILE: src/Ocelot/Infrastructure/DesignPatterns/Retry.cs ================================================ using Ocelot.Logging; namespace Ocelot.Infrastructure.DesignPatterns; /// /// Basic Retry pattern for stabilizing integrated services. /// /// Docs: /// /// Microsoft Learn | Retry pattern /// /// public static class Retry { public const int DefaultRetryTimes = 3; public const int DefaultWaitTimeMilliseconds = 25; private static string GetMessage(T operation, int retryNo, string message) where T : Delegate => $"Ocelot {nameof(Retry)} strategy for the operation of '{operation.GetType()}' type -> {nameof(Retry)} No {retryNo}: {message}"; /// /// Retry a synchronous operation when an exception occurs or predicate is true, then delay and retry again. /// /// Type of the result of the sync operation. /// Required Func-delegate of the operation. /// Predicate to check, optionally. /// Number of retries. /// Waiting time in milliseconds. /// Concrete logger from upper context. /// A value as the result of the sync operation. public static TResult Operation( Func operation, Predicate predicate = null, int retryTimes = DefaultRetryTimes, int waitTime = DefaultWaitTimeMilliseconds, IOcelotLogger logger = null) { if (waitTime < 0) { waitTime = 0; // 0 means no thread sleeping } for (int n = 1; n < retryTimes; n++) { TResult result; try { result = operation.Invoke(); } catch (Exception e) { logger?.LogError(() => GetMessage(operation, n, $"Caught exception of the {e.GetType()} type -> Message: {e.Message}."), e); Thread.Sleep(waitTime); continue; // the result is unknown, so continue to retry } // Apply predicate for known result if (predicate?.Invoke(result) == true) { logger?.LogWarning(() => GetMessage(operation, n, $"The predicate has identified erroneous state in the returned result. For further details, implement logging of the result's value or properties within the predicate method.")); Thread.Sleep(waitTime); continue; // on erroneous state } // Happy path return result; } // Last retry should generate native exception or other erroneous state(s) logger?.LogDebug(() => GetMessage(operation, retryTimes, $"Retrying lastly...")); return operation.Invoke(); // also final result must be analyzed in the upper context } /// /// Retry an asynchronous operation when an exception occurs or predicate is true, then delay and retry again. /// /// Type of the result of the async operation. /// Required Func-delegate of the operation. /// Predicate to check, optionally. /// Number of retries. /// Waiting time in milliseconds. /// Concrete logger from upper context. /// A value as the result of the async operation. public static async Task OperationAsync( Func> operation, // required operation delegate Predicate predicate = null, // optional retry predicate for the result int retryTimes = DefaultRetryTimes, int waitTime = DefaultWaitTimeMilliseconds, // retrying options IOcelotLogger logger = null) // static injections { for (int n = 1; n < retryTimes; n++) { TResult result; try { result = await operation?.Invoke(); } catch (Exception e) { logger?.LogError(() => GetMessage(operation, n, $"Caught exception of the {e.GetType()} type -> Message: {e.Message}."), e); await (waitTime > 0 ? Task.Delay(waitTime) : Task.CompletedTask); continue; // the result is unknown, so continue to retry } // Apply predicate for known result if (predicate?.Invoke(result) == true) { logger?.LogWarning(() => GetMessage(operation, n, $"The predicate has identified erroneous state in the returned result. For further details, implement logging of the result's value or properties within the predicate method.")); await (waitTime > 0 ? Task.Delay(waitTime) : Task.CompletedTask); continue; // on erroneous state } // Happy path return result; } // Last retry should generate native exception or other erroneous state(s) logger?.LogDebug(() => GetMessage(operation, retryTimes, $"Retrying lastly...")); return await operation?.Invoke(); // also final result must be analyzed in the upper context } } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/ErrorListExtensions.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure.Extensions; public static class ErrorListExtensions { private static readonly string Nl = Environment.NewLine; private static readonly string Em = string.Empty; /// /// Joins all errors using separator, in the format "Code: Message". /// /// The list of errors to extend. /// Flag to insert new line before. /// Flag to insert new line after. /// Single with all errors. public static string ToErrorString(this List errors, bool before = false, bool after = false) => (before ? Nl : Em) + string.Join(Nl, errors.Select(e => e.ToString())) + (after ? Nl : Em); } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/HttpContextExtensions.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.Infrastructure.Extensions; public static class HttpContextExtensions { public static bool IsOptionsMethod(this HttpContext context) => context.Request.IsOptionsMethod(); } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/HttpRequestExtensions.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.Infrastructure.Extensions; public static class HttpRequestExtensions { public static bool IsOptionsMethod(this HttpRequest request) => HttpMethod.Options.Method.Equals(request.Method, StringComparison.OrdinalIgnoreCase); } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/IEnumerableExtensions.cs ================================================ namespace Ocelot.Infrastructure.Extensions; public static class IEnumerableExtensions { /// /// Converts a collection of representations of HTTP methods (verbs) into a hashed set of objects. /// /// Note: /// /// Trims each string in the collection. /// Does not throw if the collection is . /// /// /// The collection of HTTP method strings. /// A object, where T is . public static HashSet ToHttpMethods(this IEnumerable collection) { collection ??= Enumerable.Empty(); return collection.Select(verb => new HttpMethod(verb.Trim())).ToHashSet(); } /// /// Helper function to convert multiple objects as strings into a comma-separated string aka CSV. /// /// The type of the objects. /// The collection of to join by comma separator. /// A in the comma-separated format. public static string Csv(this IEnumerable values) => string.Join(',', values.NotNull()); public static IEnumerable NotNull(this IEnumerable collection) => collection ?? Enumerable.Empty(); } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/Int32Extensions.cs ================================================ namespace Ocelot.Infrastructure.Extensions; public static class Int32Extensions { public static int Ensure(this int value, int low = 0) => value < low ? low : value; public static int Positive(this int value) => Ensure(value, 1); /// /// Ensures nullable integer is positive, otherwise converts the value to default one. /// /// The value. /// Default integer to convert to. /// A nullable value. public static int? Positive(this int? value, int toDefault = 1) => !value.HasValue ? null : value.Value > 0 ? value.Value : toDefault; } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/StringBuilderExtensions.cs ================================================ namespace Ocelot.Infrastructure.Extensions; public static class StringBuilderExtensions { /// Helper method to add a string to the key builder, using a comma as the default separator. /// The key builder instance. /// The next string to append. /// The character used to separate entries. /// Returns the same builder instance. public static StringBuilder AppendNext(this StringBuilder builder, string next, char separator = ',') { if (builder.Length > 0) { builder.Append(separator); } return builder.Append(next); } } ================================================ FILE: src/Ocelot/Infrastructure/Extensions/StringExtensions.cs ================================================ namespace Ocelot.Infrastructure.Extensions; public static class StringExtensions { /// Indicates whether a specified string is , empty, or consists only of white-space characters. /// This is shortcut for the method. /// The string to test. /// if the parameter is or , or if consists exclusively of white-space characters. public static bool IsEmpty(this string str) => string.IsNullOrWhiteSpace(str); public static bool IsNotEmpty(this string str) => !string.IsNullOrWhiteSpace(str); /// Defaults to the default string if the current string is null or empty. /// Based on the method. /// The current string. /// The default string. /// The string if is empty; otherwise, the string. public static string IfEmpty(this string str, string def) => string.IsNullOrWhiteSpace(str) ? def : str; /// Removes the prefix from the beginning of the string repeatedly until all occurrences are eliminated. /// The string to trim. /// The prefix string to remove. /// The 2nd argument of the method. /// A new without the prefix all occurrences. public static string TrimPrefix(this string source, string prefix, StringComparison comparison = StringComparison.Ordinal) { if (source == null || string.IsNullOrEmpty(prefix)) { return source; } var s = source; while (s.StartsWith(prefix, comparison)) { s = s[prefix.Length..]; } return s; } public const char Slash = '/'; /// Ensures that the last char of the string is forward slash, '/'. /// The string to check its last slash char. /// A witl the last forward slash. public static string LastCharAsForwardSlash(this string source) => source.EndsWith(Slash) ? source : source + Slash; public static string Plural(this int count) => count == 1 ? string.Empty : "s"; public static string Plural(this string source, int count) => count == 1 ? source : string.Concat(source, "s"); } ================================================ FILE: src/Ocelot/Infrastructure/FrameworkDescription.cs ================================================ using System.Runtime.InteropServices; namespace Ocelot.Infrastructure; public class FrameworkDescription : IFrameworkDescription { public string Get() { return RuntimeInformation.FrameworkDescription; } } ================================================ FILE: src/Ocelot/Infrastructure/IBus.cs ================================================ namespace Ocelot.Infrastructure; public interface IBus { void Subscribe(Action action); void Publish(T message, int delay); } ================================================ FILE: src/Ocelot/Infrastructure/IFrameworkDescription.cs ================================================ namespace Ocelot.Infrastructure; public interface IFrameworkDescription { string Get(); } ================================================ FILE: src/Ocelot/Infrastructure/IPlaceholders.cs ================================================ using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.Infrastructure; public interface IPlaceholders { Response Get(string key); Response Get(string key, DownstreamRequest request); Response Add(string key, Func> func); Response Remove(string key); } ================================================ FILE: src/Ocelot/Infrastructure/InMemoryBus.cs ================================================ namespace Ocelot.Infrastructure; public class InMemoryBus : IBus { private readonly BlockingCollection> _queue; private readonly List> _subscriptions; private readonly Thread _processing; public InMemoryBus() { _queue = new BlockingCollection>(); _subscriptions = new List>(); _processing = new Thread(async () => await Process()); _processing.Start(); } public void Subscribe(Action action) { _subscriptions.Add(action); } public void Publish(T message, int delay) { var delayed = new DelayedMessage(message, delay); _queue.Add(delayed); } private async Task Process() { foreach (var delayedMessage in _queue.GetConsumingEnumerable()) { await Task.Delay(delayedMessage.Delay); foreach (var subscription in _subscriptions) { subscription(delayedMessage.Message); } } } } ================================================ FILE: src/Ocelot/Infrastructure/Placeholders.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Net.Sockets; namespace Ocelot.Infrastructure; public class Placeholders : IPlaceholders { public const char OpeningBrace = '{'; public const char ClosingBrace = '}'; private readonly Dictionary>> _placeholders; private readonly Dictionary> _requestPlaceholders; private readonly IBaseUrlFinder _finder; private readonly IRequestScopedDataRepository _repo; private readonly IHttpContextAccessor _contextAccessor; public Placeholders(IBaseUrlFinder finder, IRequestScopedDataRepository repo, IHttpContextAccessor contextAccessor) { _repo = repo; _contextAccessor = contextAccessor; _finder = finder; _placeholders = new Dictionary>> { { "{BaseUrl}", GetBaseUrl }, { "{TraceId}", GetTraceId }, { "{RemoteIpAddress}", GetRemoteIpAddress }, { "{UpstreamHost}", GetUpstreamHost }, }; _requestPlaceholders = new Dictionary> { { "{DownstreamBaseUrl}", GetDownstreamBaseUrl }, }; } public Response Get(string key) { if (_placeholders.TryGetValue(key, out Func> valueFunc)) { var response = valueFunc.Invoke(); if (!response.IsError) { return new OkResponse(response.Data); } } return new ErrorResponse(new CouldNotFindPlaceholderError(key)); } public Response Get(string key, DownstreamRequest request) { return _requestPlaceholders.TryGetValue(key, out var func) ? new OkResponse(func.Invoke(request)) : new ErrorResponse(new CouldNotFindPlaceholderError(key)); } public Response Add(string key, Func> func) { return _placeholders.TryAdd(key, func) ? new OkResponse() : new ErrorResponse(new CannotAddPlaceholderError($"Unable to add placeholder: {key}, placeholder already exists")); } public Response Remove(string key) { if (!_placeholders.ContainsKey(key)) { return new ErrorResponse(new CannotRemovePlaceholderError($"Unable to remove placeholder: {key}, placeholder does not exists")); } _placeholders.Remove(key); return new OkResponse(); } private Response GetRemoteIpAddress() { // this can blow up so adding try catch and return error try { var ip = _contextAccessor.HttpContext.Connection.RemoteIpAddress ?? Dns.GetHostAddresses(string.Empty).FirstOrDefault(a => a.AddressFamily != AddressFamily.InterNetworkV6); // detect localhost network interface, a lifehack return ip != null ? new OkResponse(ip.ToString()) : new ErrorResponse(new CouldNotFindPlaceholderError("{RemoteIpAddress}")); } catch { return new ErrorResponse(new CouldNotFindPlaceholderError("{RemoteIpAddress}")); } } private static string GetDownstreamBaseUrl(DownstreamRequest x) { var downstreamUrl = $"{x.Scheme}://{x.Host}"; if (x.Port != 80 && x.Port != 443) { downstreamUrl = $"{downstreamUrl}:{x.Port}"; } return $"{downstreamUrl}/"; } private Response GetTraceId() { var traceId = _repo.Get(OcelotHttpTracingHandler.TraceId); return traceId.IsError ? new ErrorResponse(traceId.Errors) : new OkResponse(traceId.Data); } private Response GetBaseUrl() => new OkResponse(_finder.Find()); private Response GetUpstreamHost() { try { return _contextAccessor.HttpContext.Request.Headers.TryGetValue("Host", out var upstreamHost) ? new OkResponse(upstreamHost.First()) : new ErrorResponse(new CouldNotFindPlaceholderError("{UpstreamHost}")); } catch { return new ErrorResponse(new CouldNotFindPlaceholderError("{UpstreamHost}")); } } } ================================================ FILE: src/Ocelot/Infrastructure/RegexGlobal.cs ================================================ using Ocelot.DependencyInjection; namespace Ocelot.Infrastructure; public static class RegexGlobal { static RegexGlobal() { RegexCacheSize = DefaultRegexCacheSize; DefaultMatchTimeout = TimeSpan.FromMilliseconds(DefaultMatchTimeoutMilliseconds); AppDomain.CurrentDomain.SetData("REGEX_DEFAULT_MATCH_TIMEOUT", DefaultMatchTimeout); } /// Default value of the property. public const int DefaultRegexCacheSize = 100; /// Gets or sets the global value to assign to the property. /// Ocelot forcibly assigns this value during app startup, see class. /// /// Default value is 100 aka .
/// Default .NET value of is 15.
/// An value. public static int RegexCacheSize { get; set; } #pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable CS1574 // File name must match first type name /// Default value for the and the constructors. #pragma warning restore CS1574 // File name must match first type name #pragma warning restore IDE0079 // Remove unnecessary suppression public const int DefaultMatchTimeoutMilliseconds = 100; /// Default match timeout for the constructors. /// Default value is 100 ms aka . /// A value. public static TimeSpan DefaultMatchTimeout { get; set; } public static Regex New(string pattern) => new(pattern, RegexOptions.Compiled, DefaultMatchTimeout); public static Regex New(string pattern, RegexOptions options) => new(pattern, options | RegexOptions.Compiled, DefaultMatchTimeout); public static Regex New(string pattern, RegexOptions options, TimeSpan matchTimeout) => new(pattern, options | RegexOptions.Compiled, matchTimeout); } ================================================ FILE: src/Ocelot/Infrastructure/RequestData/CannotAddDataError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure.RequestData; public class CannotAddDataError : Error { public CannotAddDataError(string message) : base(message, OcelotErrorCode.CannotAddDataError, 404) { } } ================================================ FILE: src/Ocelot/Infrastructure/RequestData/CannotFindDataError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Infrastructure.RequestData; public class CannotFindDataError : Error { public CannotFindDataError(string message) : base(message, OcelotErrorCode.CannotFindDataError, 404) { } } ================================================ FILE: src/Ocelot/Infrastructure/RequestData/HttpDataRepository.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Responses; namespace Ocelot.Infrastructure.RequestData; public class HttpDataRepository : IRequestScopedDataRepository { private readonly IHttpContextAccessor _contextAccessor; public HttpDataRepository(IHttpContextAccessor contextAccessor) { _contextAccessor = contextAccessor; } public Response Add(string key, T value) { try { _contextAccessor.HttpContext.Items.Add(key, value); return new OkResponse(); } catch (Exception exception) { return new ErrorResponse(new CannotAddDataError(string.Format($"Unable to add data for key: {key}, exception: {exception.Message}"))); } } public Response Update(string key, T value) { try { _contextAccessor.HttpContext.Items[key] = value; return new OkResponse(); } catch (Exception exception) { return new ErrorResponse(new CannotAddDataError(string.Format($"Unable to update data for key: {key}, exception: {exception.Message}"))); } } public Response Get(string key) { if (_contextAccessor?.HttpContext?.Items == null) { return new ErrorResponse(new CannotFindDataError($"Unable to find data for key: {key} because HttpContext or HttpContext.Items is null")); } return _contextAccessor.HttpContext.Items.TryGetValue(key, out var item) ? new OkResponse((T)item) : new ErrorResponse(new CannotFindDataError($"Unable to find data for key: {key}")); } } ================================================ FILE: src/Ocelot/Infrastructure/RequestData/IRequestScopedDataRepository.cs ================================================ using Ocelot.Responses; namespace Ocelot.Infrastructure.RequestData; public interface IRequestScopedDataRepository { Response Add(string key, T value); Response Update(string key, T value); Response Get(string key); } ================================================ FILE: src/Ocelot/LoadBalancer/Balancers/CookieStickySessions.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.LoadBalancer.Balancers; public class CookieStickySessions : ILoadBalancer { /// /// Track default ASP.NET Core session idle timeout here: SessionOptions.IdleTimeout. /// public const int DefSessionExpiryMinutes = 20; public static readonly int DefSessionExpiryMilliseconds; /// /// Track default ASP.NET Core session cookie name here: SessionDefaults.CookieName. /// public static readonly string DefSessionCookieName = Microsoft.AspNetCore.Session.SessionDefaults.CookieName; static CookieStickySessions() { #if NET9_0_OR_GREATER DefSessionExpiryMilliseconds = DefSessionExpiryMinutes * (int)TimeSpan.MillisecondsPerMinute; #else // TODO Migrate to TimeSpan.MillisecondsPerMinute after net8.0 deprecation DefSessionExpiryMilliseconds = (int)TimeSpan.FromMinutes(DefSessionExpiryMinutes).TotalMilliseconds; #endif } private readonly int _keyExpiryInMs; private readonly string _cookieName; private readonly ILoadBalancer _loadBalancer; private readonly IBus _bus; #if NET9_0_OR_GREATER private static readonly Lock Locker = new(); #else private static readonly object Locker = new(); #endif private static readonly Dictionary Stored = new(); // TODO Inject instead of static sharing public string Type => nameof(CookieStickySessions); public CookieStickySessions(ILoadBalancer loadBalancer, string cookieName, int keyExpiryInMs, IBus bus) { _bus = bus; _cookieName = cookieName; _keyExpiryInMs = keyExpiryInMs; _loadBalancer = loadBalancer; _bus.Subscribe(CheckExpiry); } private void CheckExpiry(StickySession sticky) { // TODO Get test coverage for this lock (Locker) { if (!Stored.TryGetValue(sticky.Key, out var session) || session.Expiry >= DateTime.UtcNow) { return; } Stored.Remove(session.Key); _loadBalancer.Release(session.HostAndPort); } } public Task> LeaseAsync(HttpContext httpContext) { var route = httpContext.Items.DownstreamRoute(); var serviceName = route.LoadBalancerKey; var cookie = httpContext.Request.Cookies[_cookieName]; var key = $"{serviceName}:{cookie}"; // strong key name because of static store lock (Locker) { if (Stored.TryGetValue(key, out StickySession cached)) { var updated = new StickySession(cached.HostAndPort, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key); Update(key, updated); return Task.FromResult>(new OkResponse(updated.HostAndPort)); } // There is no value in the store, so lease it now! var next = _loadBalancer.LeaseAsync(httpContext).GetAwaiter().GetResult(); // unfortunately the operation must be synchronous if (next.IsError) { return Task.FromResult>(new ErrorResponse(next.Errors)); } var ss = new StickySession(next.Data, DateTime.UtcNow.AddMilliseconds(_keyExpiryInMs), key); Update(key, ss); return Task.FromResult>(new OkResponse(next.Data)); } } protected void Update(string key, StickySession value) { lock (Locker) { Stored[key] = value; _bus.Publish(value, _keyExpiryInMs); } } public void Release(ServiceHostAndPort hostAndPort) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Balancers/LeastConnection.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.LoadBalancer.Balancers; public class LeastConnection : ILoadBalancer { private readonly Func>> _services; private readonly List _leases; private readonly string _serviceName; #if NET9_0_OR_GREATER private static readonly Lock SyncRoot = new(); #else private static readonly object SyncRoot = new(); #endif public string Type => nameof(LeastConnection); public LeastConnection(Func>> services, string serviceName) { _services = services; _serviceName = serviceName; _leases = new List(); } public event EventHandler Leased; protected virtual void OnLeased(LeaseEventArgs e) => Leased?.Invoke(this, e); public async Task> LeaseAsync(HttpContext httpContext) { var services = await _services.Invoke(); if ((services?.Count ?? 0) == 0) { return new ErrorResponse(new ServicesAreNullError($"Services were null/empty in {Type} for '{_serviceName}' during {nameof(LeaseAsync)} operation!")); } lock (SyncRoot) { //todo - maybe this should be moved somewhere else...? Maybe on a repeater on seperate thread? loop every second and update or something? UpdateLeasing(services); Lease wanted = GetLeaseWithLeastConnections(); _ = Update(ref wanted, true); var index = services.FindIndex(s => s.HostAndPort == wanted); OnLeased(new(wanted, services[index], index)); return new OkResponse(new(wanted.HostAndPort)); } } public void Release(ServiceHostAndPort hostAndPort) { lock (SyncRoot) { var matchingLease = _leases.Find(l => l == hostAndPort); if (matchingLease != Lease.Null) { _ = Update(ref matchingLease, false); } } } private int Update(ref Lease item, bool increase) { var index = _leases.IndexOf(item); _ = increase ? item.Connections++ : item.Connections--; _leases[index] = item; // write the value back to the position return index; } private Lease GetLeaseWithLeastConnections() { var min = _leases.Min(l => l.Connections); return _leases.Find(l => l.Connections == min); } private void UpdateLeasing(List services) { if (_leases.Count > 0) { _leases.RemoveAll(l => !services.Exists(s => s.HostAndPort == l)); services.Where(s => !_leases.Exists(l => l == s.HostAndPort)) .ToList() .ForEach(s => _leases.Add(new(s.HostAndPort, 0))); } else { services.ForEach(s => _leases.Add(new(s.HostAndPort))); } } } ================================================ FILE: src/Ocelot/LoadBalancer/Balancers/NoLoadBalancer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.LoadBalancer.Balancers; public class NoLoadBalancer : ILoadBalancer { private readonly Func>> _services; public NoLoadBalancer(Func>> services) { _services = services; } public string Type => nameof(NoLoadBalancer); public async Task> LeaseAsync(HttpContext httpContext) { var services = await _services(); if (services == null || services.Count == 0) { return new ErrorResponse(new ServicesAreEmptyError($"There were no services in {Type}!")); } var service = await Task.FromResult(services.FirstOrDefault()); return new OkResponse(service.HostAndPort); } public void Release(ServiceHostAndPort hostAndPort) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Balancers/RoundRobin.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.LoadBalancer.Balancers; public class RoundRobin : ILoadBalancer { private readonly Func>> _servicesDelegate; private readonly string _serviceName; private readonly List _leasing; public string Type => nameof(RoundRobin); public RoundRobin(Func>> services, string serviceName) { ArgumentNullException.ThrowIfNull(services); _servicesDelegate = services; _serviceName = serviceName; _leasing = new(); } private static readonly Dictionary LastIndices = new(); #if NET9_0_OR_GREATER private static readonly Lock SyncRoot = new(); #else private static readonly object SyncRoot = new(); #endif public event EventHandler Leased; protected virtual void OnLeased(LeaseEventArgs e) => Leased?.Invoke(this, e); public virtual async Task> LeaseAsync(HttpContext httpContext) { var services = await _servicesDelegate.Invoke() ?? new(0); if (services.Count == 0) { return new ErrorResponse(new ServicesAreEmptyError($"There were no services in {Type} for '{_serviceName}' during {nameof(LeaseAsync)} operation!")); } lock (SyncRoot) { var readMe = CaptureState(services, out int count); if (!TryScanNext(readMe, out Service next, out int index)) { return new ErrorResponse(new ServicesAreNullError($"The service at index {index} was null in {Type} for {_serviceName} during the {nameof(LeaseAsync)} operation. Total services count: {count}.")); } ProcessLeasing(readMe, next, index); // Happy path: Lease now return new OkResponse(next.HostAndPort); } } public virtual void Release(ServiceHostAndPort hostAndPort) { } /// Capture the count value because another thread might modify the list. /// Mutable collection of services. /// Captured count value. /// Captured collection as a object. private static Service[] CaptureState(List services, out int count) { // Capture the count value because another thread might modify the list count = services.Count; var readMe = new Service[count]; services.CopyTo(readMe); return readMe; } /// Scan for the next online service instance which must be healthy. /// Read-only collection. /// The next online service to return. /// The index of the next service to return. /// if found next online service; otherwise . private bool TryScanNext(Service[] readme, out Service next, out int index) { int length = readme.Length, stop = length; LastIndices.TryGetValue(_serviceName, out int last); if (last >= length) { last = 0; } next = null; index = last; // Scan for the next service instance // TODO Check real health status while (next?.HostAndPort == null && stop-- > 0) { index = last; next = readme[last]; LastIndices[_serviceName] = (++last < length) ? last : 0; } return next != null; } private void ProcessLeasing(Service[] readme, Service next, int index) { UpdateLeasing(readme); Lease wanted = GetLease(next); _ = Update(ref wanted, true); // perform counting based on Connections OnLeased(new(wanted, next, index)); } private int Update(ref Lease item, bool increase) { var index = _leasing.IndexOf(item); _ = increase ? item.Connections++ : item.Connections--; _leasing[index] = item; // write the value back to the position return index; } private Lease GetLease(Service @for) => _leasing.Find(l => l == @for.HostAndPort); private void UpdateLeasing(IList services) { // Don't remove leasing data of old services, so keep data during life time of the load balancer // _leasing.RemoveAll(l => services.All(s => s?.HostAndPort != l)); var newLeases = services .Where(s => s != null && !_leasing.Exists(l => l == s.HostAndPort)) .Select(s => new Lease(s.HostAndPort)) .ToArray(); // capture leasing state and produce new collection _leasing.AddRange(newLeases); } } ================================================ FILE: src/Ocelot/LoadBalancer/Creators/CookieStickySessionsCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.Infrastructure; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Creators; public class CookieStickySessionsCreator : ILoadBalancerCreator { public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { var options = route.LoadBalancerOptions; var loadBalancer = new RoundRobin(serviceProvider.GetAsync, route.LoadBalancerKey); var bus = new InMemoryBus(); return new OkResponse( new CookieStickySessions(loadBalancer, options.Key, options.ExpiryInMs, bus)); } public string Type => nameof(CookieStickySessions); } ================================================ FILE: src/Ocelot/LoadBalancer/Creators/DelegateInvokingLoadBalancerCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Creators; public class DelegateInvokingLoadBalancerCreator : ILoadBalancerCreator where T : ILoadBalancer { private readonly Func _creatorFunc; public DelegateInvokingLoadBalancerCreator( Func creatorFunc) { _creatorFunc = creatorFunc; } public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { try { return new OkResponse(_creatorFunc(route, serviceProvider)); } catch (Exception e) { return new ErrorResponse(new InvokingLoadBalancerCreatorError(e)); } } public string Type => typeof(T).Name; } ================================================ FILE: src/Ocelot/LoadBalancer/Creators/LeastConnectionCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Creators; public class LeastConnectionCreator : ILoadBalancerCreator { public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { var loadBalancer = new LeastConnection( serviceProvider.GetAsync, !string.IsNullOrEmpty(route.ServiceName) ? route.ServiceName : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key return new OkResponse(loadBalancer); } public string Type => nameof(LeastConnection); } ================================================ FILE: src/Ocelot/LoadBalancer/Creators/NoLoadBalancerCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Creators; public class NoLoadBalancerCreator : ILoadBalancerCreator { public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { return new OkResponse(new NoLoadBalancer(async () => await serviceProvider.GetAsync())); } public string Type => nameof(NoLoadBalancer); } ================================================ FILE: src/Ocelot/LoadBalancer/Creators/RoundRobinCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Creators; public class RoundRobinCreator : ILoadBalancerCreator { public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { var loadBalancer = new RoundRobin( serviceProvider.GetAsync, !string.IsNullOrEmpty(route.ServiceName) ? route.ServiceName : route.LoadBalancerKey); // if service discovery mode then use service name; otherwise use balancer key return new OkResponse(loadBalancer); } public string Type => nameof(RoundRobin); } ================================================ FILE: src/Ocelot/LoadBalancer/Errors/CouldNotFindLoadBalancerCreatorError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.LoadBalancer.Errors; public class CouldNotFindLoadBalancerCreatorError : Error { public CouldNotFindLoadBalancerCreatorError(string message) : base(message, OcelotErrorCode.CouldNotFindLoadBalancerCreator, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Errors/InvokingLoadBalancerCreatorError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.LoadBalancer.Errors; public class InvokingLoadBalancerCreatorError : Error { public InvokingLoadBalancerCreatorError(Exception e) : base($"Error when invoking user provided load balancer creator function, Message: {e.Message}, StackTrace: {e.StackTrace}", OcelotErrorCode.ErrorInvokingLoadBalancerCreator, StatusCodes.Status500InternalServerError) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Errors/ServicesAreEmptyError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.LoadBalancer.Errors; public class ServicesAreEmptyError : Error { public ServicesAreEmptyError(string message) : base(message, OcelotErrorCode.ServicesAreEmptyError, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Errors/ServicesAreNullError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.LoadBalancer.Errors; public class ServicesAreNullError : Error { public ServicesAreNullError(string message) : base(message, OcelotErrorCode.ServicesAreNullError, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Errors/UnableToFindLoadBalancerError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.LoadBalancer.Errors; public class UnableToFindLoadBalancerError : Error { public UnableToFindLoadBalancerError(string message) : base(message, OcelotErrorCode.UnableToFindLoadBalancerError, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/LoadBalancer/Interfaces/ILoadBalancer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Responses; using Ocelot.Values; using System.Reflection; namespace Ocelot.LoadBalancer.Interfaces; // TODO Add sync & async pairs public interface ILoadBalancer { Task> LeaseAsync(HttpContext httpContext); void Release(ServiceHostAndPort hostAndPort); /// Static name of the load balancer instance. /// To avoid reflection calls of the property of the objects. /// A object with type name value. string Type { get; } } ================================================ FILE: src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.LoadBalancer.Interfaces; public interface ILoadBalancerCreator { Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider); string Type { get; } } ================================================ FILE: src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerFactory.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.LoadBalancer.Interfaces; public interface ILoadBalancerFactory { Response Get(DownstreamRoute route, ServiceProviderConfiguration config); } ================================================ FILE: src/Ocelot/LoadBalancer/Interfaces/ILoadBalancerHouse.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.LoadBalancer.Interfaces; public interface ILoadBalancerHouse { Response Get(DownstreamRoute route, ServiceProviderConfiguration config); } ================================================ FILE: src/Ocelot/LoadBalancer/Lease.cs ================================================ using Ocelot.Values; namespace Ocelot.LoadBalancer; public struct Lease : IEquatable { public Lease() { HostAndPort = null; Connections = 0; } public Lease(Lease from) { HostAndPort = from.HostAndPort; Connections = from.Connections; } public Lease(ServiceHostAndPort hostAndPort) { HostAndPort = hostAndPort; Connections = 0; } public Lease(ServiceHostAndPort hostAndPort, int connections) { HostAndPort = hostAndPort; Connections = connections; } public ServiceHostAndPort HostAndPort { get; } public int Connections { get; set; } public static Lease Null => new(); public override readonly string ToString() => $"({HostAndPort}+{Connections})"; public override readonly int GetHashCode() => HostAndPort.GetHashCode(); public override readonly bool Equals(object obj) => obj is Lease l && this == l; public readonly bool Equals(Lease other) => this == other; /// Checks equality of two leases. /// /// Override default implementation of because we want to ignore the property. /// Microsoft Learn | .NET | C# Docs: /// /// Equality operators /// System.Object.Equals method /// IEquatable<T>.Equals(T) Method /// ValueType.Equals(Object) Method /// /// /// First operand. /// Second operand. /// if both operands are equal; otherwise, . public static bool operator ==(Lease x, Lease y) => x.HostAndPort == y.HostAndPort; // ignore -> x.Connections == y.Connections; public static bool operator !=(Lease x, Lease y) => !(x == y); public static bool operator ==(ServiceHostAndPort h, Lease l) => h == l.HostAndPort; public static bool operator !=(ServiceHostAndPort h, Lease l) => !(h == l); public static bool operator ==(Lease l, ServiceHostAndPort h) => l.HostAndPort == h; public static bool operator !=(Lease l, ServiceHostAndPort h) => !(l == h); } ================================================ FILE: src/Ocelot/LoadBalancer/LeaseEventArgs.cs ================================================ using Ocelot.Values; namespace Ocelot.LoadBalancer; public class LeaseEventArgs : EventArgs { public LeaseEventArgs(Lease lease, Service service, int serviceIndex) { Lease = lease; Service = service; ServiceIndex = serviceIndex; } public Lease Lease { get; } public Service Service { get; } public int ServiceIndex { get; } } ================================================ FILE: src/Ocelot/LoadBalancer/LoadBalancerFactory.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery; namespace Ocelot.LoadBalancer; public class LoadBalancerFactory : ILoadBalancerFactory { private readonly IServiceDiscoveryProviderFactory _serviceProviderFactory; private readonly IEnumerable _loadBalancerCreators; public LoadBalancerFactory(IServiceDiscoveryProviderFactory serviceProviderFactory, IEnumerable loadBalancerCreators) { _serviceProviderFactory = serviceProviderFactory; _loadBalancerCreators = loadBalancerCreators; } public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) { var serviceProviderFactoryResponse = _serviceProviderFactory.Get(config, route); if (serviceProviderFactoryResponse.IsError) { return new ErrorResponse(serviceProviderFactoryResponse.Errors); } var serviceProvider = serviceProviderFactoryResponse.Data; var requestedType = route.LoadBalancerOptions?.Type ?? nameof(NoLoadBalancer); var applicableCreator = _loadBalancerCreators.SingleOrDefault(c => c.Type == requestedType); if (applicableCreator == null) { return new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: {requestedType}, please check your config specified the correct load balancer and that you have registered a class with the same name.")); } var createdLoadBalancerResponse = applicableCreator.Create(route, serviceProvider); if (createdLoadBalancerResponse.IsError) { return new ErrorResponse(createdLoadBalancerResponse.Errors); } return new OkResponse(createdLoadBalancerResponse.Data); } } ================================================ FILE: src/Ocelot/LoadBalancer/LoadBalancerHouse.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; namespace Ocelot.LoadBalancer; public class LoadBalancerHouse : ILoadBalancerHouse { private readonly ILoadBalancerFactory _factory; private readonly Dictionary _loadBalancers; #if NET9_0_OR_GREATER private static readonly Lock SyncRoot = new(); #else private static readonly object SyncRoot = new(); #endif public LoadBalancerHouse(ILoadBalancerFactory factory) { _factory = factory; _loadBalancers = new(); } public Response Get(DownstreamRoute route, ServiceProviderConfiguration config) { try { lock (SyncRoot) { return _loadBalancers.TryGetValue(route.LoadBalancerKey, out var loadBalancer) && loadBalancer.Type.Equals(route.LoadBalancerOptions.Type, StringComparison.OrdinalIgnoreCase) ? new OkResponse(loadBalancer) : GetResponse(route, config); } } catch (Exception ex) { return new ErrorResponse( new UnableToFindLoadBalancerError($"Unable to find load balancer for '{route.LoadBalancerKey}'. Exception: {ex};")); } } private Response GetResponse(DownstreamRoute route, ServiceProviderConfiguration config) { var result = _factory.Get(route, config); if (result.IsError) { return new ErrorResponse(result.Errors); } var balancer = result.Data; _loadBalancers[route.LoadBalancerKey] = balancer; // TODO TryAdd ? return new OkResponse(balancer); } } ================================================ FILE: src/Ocelot/LoadBalancer/LoadBalancingMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.LoadBalancer; public class LoadBalancingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly ILoadBalancerHouse _loadBalancerHouse; public LoadBalancingMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, ILoadBalancerHouse loadBalancerHouse) : base(loggerFactory.CreateLogger()) { _next = next; _loadBalancerHouse = loadBalancerHouse; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); var internalConfiguration = httpContext.Items.IInternalConfiguration(); var loadBalancer = _loadBalancerHouse.Get(downstreamRoute, internalConfiguration.ServiceProviderConfiguration); if (loadBalancer.IsError) { httpContext.Items.UpsertErrors(loadBalancer.Errors); return; } var hostAndPort = await loadBalancer.Data.LeaseAsync(httpContext); if (hostAndPort.IsError) { httpContext.Items.UpsertErrors(hostAndPort.Errors); return; } var downstreamRequest = httpContext.Items.DownstreamRequest(); //todo check downstreamRequest is ok downstreamRequest.Host = hostAndPort.Data.DownstreamHost; if (hostAndPort.Data.DownstreamPort > 0) { downstreamRequest.Port = hostAndPort.Data.DownstreamPort; } if (!string.IsNullOrEmpty(hostAndPort.Data.Scheme)) { downstreamRequest.Scheme = hostAndPort.Data.Scheme; } try { // If an exception occurs, the object will be handled by the global exception handler await _next.Invoke(httpContext); } finally { loadBalancer.Data.Release(hostAndPort.Data); } } } ================================================ FILE: src/Ocelot/LoadBalancer/StickySession.cs ================================================ using Ocelot.Values; namespace Ocelot.LoadBalancer; public class StickySession { public StickySession(ServiceHostAndPort hostAndPort, DateTime expiry, string key) { HostAndPort = hostAndPort; Expiry = expiry; Key = key; } public ServiceHostAndPort HostAndPort { get; } public DateTime Expiry { get; } public string Key { get; } } ================================================ FILE: src/Ocelot/Logging/IOcelotLogger.cs ================================================ using Ocelot.Configuration; using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging; /// /// Thin wrapper around the .NET Core logging framework, used to allow the object to be injected giving access to the Ocelot . /// public interface IOcelotLogger : IDisposable { void LogTrace(string message); void LogTrace(Func messageFactory); void LogDebug(string message); void LogDebug(Func messageFactory); void LogInformation(string message); void LogInformation(Func messageFactory); void LogWarning(string message); void LogWarning(Func messageFactory); void LogError(string message, Exception exception); void LogError(Func messageFactory, Exception exception); void LogCritical(string message, Exception exception); void LogCritical(Func messageFactory, Exception exception); } ================================================ FILE: src/Ocelot/Logging/IOcelotLoggerFactory.cs ================================================ namespace Ocelot.Logging; public interface IOcelotLoggerFactory : IDisposable { IOcelotLogger CreateLogger(); } ================================================ FILE: src/Ocelot/Logging/IOcelotTracer.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.Logging; public interface IOcelotTracer { void Event(HttpContext httpContext, string @event); Task SendAsync(HttpRequestMessage request, Action addTraceIdToRepo, Func> baseSendAsync, CancellationToken cancellationToken); } ================================================ FILE: src/Ocelot/Logging/ITracingHandler.cs ================================================ namespace Ocelot.Logging; public interface ITracingHandler { } ================================================ FILE: src/Ocelot/Logging/ITracingHandlerFactory.cs ================================================ namespace Ocelot.Logging; public interface ITracingHandlerFactory { ITracingHandler Get(); } ================================================ FILE: src/Ocelot/Logging/OcelotDiagnosticListener.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DiagnosticAdapter; namespace Ocelot.Logging; public class OcelotDiagnosticListener { private readonly IOcelotLogger _logger; private readonly IOcelotTracer _tracer; public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider serviceProvider) { _logger = factory.CreateLogger(); _tracer = serviceProvider.GetService(); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { _logger.LogTrace(() => $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); Event(httpContext, $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public virtual void OnMiddlewareException(Exception exception, string name) { _logger.LogTrace(() => $"MiddlewareException: {name}; {exception.Message};"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] public virtual void OnMiddlewareFinished(HttpContext httpContext, string name) { _logger.LogTrace(() => $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); Event(httpContext, $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); } protected virtual void Event(HttpContext httpContext, string @event) { _tracer?.Event(httpContext, @event); } } ================================================ FILE: src/Ocelot/Logging/OcelotHttpTracingHandler.cs ================================================ using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging; public class OcelotHttpTracingHandler : DelegatingHandler, ITracingHandler { public const string TraceId = nameof(TraceId); private readonly IOcelotTracer _tracer; private readonly IRequestScopedDataRepository _repo; public OcelotHttpTracingHandler( IOcelotTracer tracer, IRequestScopedDataRepository repo, HttpMessageHandler httpMessageHandler = null) { _tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); _repo = repo ?? throw new ArgumentNullException(nameof(repo)); InnerHandler = httpMessageHandler ?? new HttpClientHandler(); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => _tracer.SendAsync(request, AddTraceId, base.SendAsync, cancellationToken); // TODO This is absolutely wrong protected virtual void AddTraceId(string id) => _repo.Add(TraceId, id); } ================================================ FILE: src/Ocelot/Logging/OcelotLogger.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.RequestId.Middleware; namespace Ocelot.Logging; /// /// Default implementation of the interface. /// public class OcelotLogger : IOcelotLogger, IDisposable { private readonly ILogger _logger; private readonly IRequestScopedDataRepository _scopedDataRepository; private bool _disposed; /// /// Initializes a new instance of the class. /// /// Please note: /// the log event message is designed to use placeholders ({RequestId}, {PreviousRequestId}, and {Message}). /// If you're using a logger like Serilog, it will automatically capture these as structured data properties, making it easier to query and analyze the logs later. /// /// /// The main logger type, per default the Microsoft implementation. /// Repository, saving and getting data to/from HttpContext.Items. /// The ILogger object is injected in OcelotLoggerFactory, it can't be verified before. public OcelotLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _scopedDataRepository = scopedDataRepository ?? throw new ArgumentNullException(nameof(scopedDataRepository)); } public void LogTrace(string message) => WriteLog(LogLevel.Trace, message); public void LogTrace(Func messageFactory) => WriteLog(LogLevel.Trace, messageFactory); public void LogDebug(string message) => WriteLog(LogLevel.Debug, message); public void LogDebug(Func messageFactory) => WriteLog(LogLevel.Debug, messageFactory); public void LogInformation(string message) => WriteLog(LogLevel.Information, message); public void LogInformation(Func messageFactory) => WriteLog(LogLevel.Information, messageFactory); public void LogWarning(string message) => WriteLog(LogLevel.Warning, message); public void LogWarning(Func messageFactory) => WriteLog(LogLevel.Warning, messageFactory); public void LogError(string message, Exception exception) => WriteLog(LogLevel.Error, message, exception); public void LogError(Func messageFactory, Exception exception) => WriteLog(LogLevel.Error, messageFactory, exception); public void LogCritical(string message, Exception exception) => WriteLog(LogLevel.Critical, message, exception); public void LogCritical(Func messageFactory, Exception exception) => WriteLog(LogLevel.Critical, messageFactory, exception); private string GetOcelotRequestId() { var requestId = _scopedDataRepository.Get(RequestIdMiddleware.RequestIdName); return requestId.IsError ? "-" : requestId.Data; } private string GetOcelotPreviousRequestId() { var requestId = _scopedDataRepository.Get(RequestIdMiddleware.PreviousRequestIdName); return requestId.IsError ? "-" : requestId.Data; } private void WriteLog(LogLevel logLevel, string message, Exception exception = null) { WriteLog(logLevel, null, message, exception); } private void WriteLog(LogLevel logLevel, Func messageFactory, Exception exception = null) { WriteLog(logLevel, messageFactory, null, exception); } private void WriteLog(LogLevel logLevel, Func messageFactory, string message, Exception exception = null) { if (_disposed || !_logger.IsEnabled(logLevel)) return; var requestId = GetOcelotRequestId(); var previousRequestId = GetOcelotPreviousRequestId(); if (messageFactory != null) { message = messageFactory.Invoke(); } try { _logger.Log(logLevel, default, $"{RequestIdMiddleware.RequestIdName}: {requestId}, {RequestIdMiddleware.PreviousRequestIdName}: {previousRequestId}{Environment.NewLine + message}", exception, NoFormatter); } catch (ObjectDisposedException) { // Logger factory or its providers have been disposed. // This can happen when errors occur in background operations. // Silently ignore to prevent cascading failures during shutdown. } } public static string NoFormatter(string state, Exception e) => state; public static string ExceptionFormatter(string state, Exception e) => e == null ? state : $"{state}, {Environment.NewLine + nameof(Exception)}: {e}"; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { } _disposed = true; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } ================================================ FILE: src/Ocelot/Logging/OcelotLoggerFactory.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging; public class OcelotLoggerFactory : IOcelotLoggerFactory, IDisposable { private readonly ILoggerFactory _loggerFactory; private readonly IRequestScopedDataRepository _scopedDataRepository; private bool _disposed; public OcelotLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) { _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); _scopedDataRepository = scopedDataRepository ?? throw new ArgumentNullException(nameof(scopedDataRepository)); } public IOcelotLogger CreateLogger() { ObjectDisposedException.ThrowIf(_disposed, this); var logger = _loggerFactory.CreateLogger(); return new OcelotLogger(logger, _scopedDataRepository); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _loggerFactory.Dispose(); } _disposed = true; } ~OcelotLoggerFactory() => Dispose(false); } ================================================ FILE: src/Ocelot/Logging/TracingHandlerFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Infrastructure.RequestData; namespace Ocelot.Logging; public class TracingHandlerFactory : ITracingHandlerFactory { private readonly IOcelotTracer _tracer; private readonly IRequestScopedDataRepository _repo; public TracingHandlerFactory( IServiceProvider services, IRequestScopedDataRepository repo) { _repo = repo; _tracer = services.GetService(); } public ITracingHandler Get() { return new OcelotHttpTracingHandler(_tracer, _repo); } } ================================================ FILE: src/Ocelot/Metadata/DownstreamRouteExtensions.cs ================================================ using Ocelot.Configuration; using System.Globalization; using System.Text.Json; using System.Text.Json.Nodes; namespace Ocelot.Metadata; public static class DownstreamRouteExtensions { /// /// The known truthy values. /// private static readonly HashSet TruthyValues = new(StringComparer.OrdinalIgnoreCase) { "true", "yes", "on", "ok", "enable", "enabled", "1", }; /// /// The known falsy values. /// private static readonly HashSet FalsyValues = new(StringComparer.OrdinalIgnoreCase) { "false", "no", "off", "disable", "disabled", "0", }; /// /// The known numeric types. /// private static readonly HashSet NumericTypes = new() { typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal), }; /// /// Gets metadata from a downstream route. /// /// The metadata target type. /// The current downstream route. /// The metadata key in downstream route Metadata dictionary. /// The fallback value if no value found. /// Custom json serializer options if needed. /// A parsed metadata value of the type. public static T GetMetadata(this DownstreamRoute route, string key, T defaultValue = default, JsonSerializerOptions options = null) { var metadata = route?.MetadataOptions.Metadata; return (metadata == null || !metadata.TryGetValue(key, out var metadataValue) || metadataValue == null) ? defaultValue : (T)ConvertTo(typeof(T), metadataValue, route.MetadataOptions, options ?? new(JsonSerializerDefaults.Web)); } // TODO See Metadata sample private static JsonNode GetMetadataNode(this DownstreamRoute route, string key, JsonNode defaultValue = default, JsonSerializerOptions options = null) { var metadata = route?.MetadataOptions.Metadata; return null; } /// /// Converting a string value to the target type. /// Some custom conversion has been for the following types: /// , , , numeric types; /// otherwise trying to deserialize the value using the JsonSerializer. /// /// The target type. /// The string value. /// The metadata options, it includes the global configuration. /// If needed, some custom json serializer options. /// The converted string. private static object ConvertTo(Type targetType, string value, MetadataOptions options, JsonSerializerOptions jsonOptions) { if (targetType == typeof(string)) { return value; } if (targetType == typeof(bool)) { return TruthyValues.Contains(value.Trim()); } if (targetType == typeof(bool?)) { return TruthyValues.Contains(value.Trim()) ? true : FalsyValues.Contains(value.Trim()) ? false : null; } if (targetType == typeof(string[])) { return (value == null) ? Array.Empty() : value.Split(options.Separators, options.StringSplitOption) .Select(s => s.Trim(options.TrimChars)) .Where(s => !string.IsNullOrEmpty(s)) .ToArray(); } return NumericTypes.Contains(targetType) ? ConvertToNumericType(value, targetType, options.CurrentCulture, options.NumberStyle) : JsonSerializer.Deserialize(value, targetType, jsonOptions); } /// /// Converting string to the known numeric types. /// /// The number as string. /// The target numeric type. /// The current format provider. /// The current number style configuration. /// The parsed string as object of type targetType. /// Exception thrown if the conversion for the type target type can't be found. private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, NumberStyles numberStyle) { return targetType switch { { } t when t == typeof(byte) => byte.Parse(value, numberStyle, provider), { } t when t == typeof(sbyte) => sbyte.Parse(value, numberStyle, provider), { } t when t == typeof(short) => short.Parse(value, numberStyle, provider), { } t when t == typeof(ushort) => ushort.Parse(value, numberStyle, provider), { } t when t == typeof(int) => int.Parse(value, numberStyle, provider), { } t when t == typeof(uint) => uint.Parse(value, numberStyle, provider), { } t when t == typeof(long) => long.Parse(value, numberStyle, provider), { } t when t == typeof(ulong) => ulong.Parse(value, numberStyle, provider), { } t when t == typeof(float) => float.Parse(value, numberStyle, provider), { } t when t == typeof(double) => double.Parse(value, numberStyle, provider), { } t when t == typeof(decimal) => decimal.Parse(value, numberStyle, provider), _ => throw new NotImplementedException($"No conversion available for the type: {targetType.Name}"), }; } } ================================================ FILE: src/Ocelot/Middleware/BaseUrlFinder.cs ================================================ using Microsoft.Extensions.Configuration; using Ocelot.Configuration.File; namespace Ocelot.Middleware; public class BaseUrlFinder : IBaseUrlFinder { private readonly IConfiguration _config; public BaseUrlFinder(IConfiguration config) { _config = config; } public string Find() { // Tries to get base url out of file... var key = $"{nameof(FileConfiguration.GlobalConfiguration)}:{nameof(FileGlobalConfiguration.BaseUrl)}"; var baseUrl = _config.GetValue(key, string.Empty); // Falls back to memory config then finally default.. return string.IsNullOrEmpty(baseUrl) ? _config.GetValue(nameof(FileGlobalConfiguration.BaseUrl), "http://localhost:5000") : baseUrl; } } ================================================ FILE: src/Ocelot/Middleware/ConfigurationMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.Repository; using Ocelot.Errors.Middleware; using Ocelot.Logging; namespace Ocelot.Middleware; public class ConfigurationMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IInternalConfigurationRepository _configRepo; public ConfigurationMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IInternalConfigurationRepository configRepo) : base(loggerFactory.CreateLogger()) { _next = next; _configRepo = configRepo; } public async Task Invoke(HttpContext httpContext) { //todo check the config is actually ok? var config = _configRepo.Get(); if (config.IsError) { throw new System.Exception("OOOOPS this should not happen raise an issue in GitHub"); } httpContext.Items.SetIInternalConfiguration(config.Data); await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/Middleware/DownstreamResponse.cs ================================================ namespace Ocelot.Middleware; public class DownstreamResponse : IDisposable { // To detect redundant calls private bool _disposedValue; private readonly HttpResponseMessage _responseMessage; public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, List
headers, string reasonPhrase) { Content = content; StatusCode = statusCode; Headers = headers ?? new(); ReasonPhrase = reasonPhrase; } public DownstreamResponse(HttpResponseMessage response) : this(response.Content, response.StatusCode, response.Headers.Select(x => new Header(x.Key, x.Value)).ToList(), response.ReasonPhrase) { _responseMessage = response; } public DownstreamResponse(HttpContent content, HttpStatusCode statusCode, IEnumerable>> headers, string reasonPhrase) : this(content, statusCode, headers.Select(x => new Header(x.Key, x.Value)).ToList(), reasonPhrase) { } public HttpContent Content { get; } public HttpStatusCode StatusCode { get; } public List
Headers { get; } public string ReasonPhrase { get; } // Public implementation of Dispose pattern callable by consumers. public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// We should make sure we dispose the content and response message to close the connection to the downstream service. /// protected virtual void Dispose(bool disposing) { if (_disposedValue) { return; } if (disposing) { Content?.Dispose(); _responseMessage?.Dispose(); } _disposedValue = true; } } ================================================ FILE: src/Ocelot/Middleware/Header.cs ================================================ namespace Ocelot.Middleware; public class Header { public Header(string key, IEnumerable values) { Key = key; Values = values ?? new List(); } public string Key { get; } public IEnumerable Values { get; } } ================================================ FILE: src/Ocelot/Middleware/HttpItemsExtensions.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; using Ocelot.Request.Middleware; namespace Ocelot.Middleware; public static class HttpItemsExtensions { public static void UpsertDownstreamRequest(this IDictionary input, DownstreamRequest downstreamRequest) { input.Upsert("DownstreamRequest", downstreamRequest); } public static void UpsertDownstreamResponse(this IDictionary input, DownstreamResponse downstreamResponse) { input.Upsert("DownstreamResponse", downstreamResponse); } public static void UpsertDownstreamRoute(this IDictionary input, DownstreamRoute downstreamRoute) { input.Upsert("DownstreamRoute", downstreamRoute); } public static void UpsertTemplatePlaceholderNameAndValues(this IDictionary input, List tPNV) { input.Upsert("TemplatePlaceholderNameAndValues", tPNV); } public static void UpsertDownstreamRoute(this IDictionary input, DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { input.Upsert("DownstreamRouteHolder", downstreamRoute); } public static void UpsertErrors(this IDictionary input, List errors) { input.Upsert("Errors", errors); } public static void SetError(this IDictionary input, Error error) { var errors = new List { error }; input.Upsert("Errors", errors); } public static void SetIInternalConfiguration(this IDictionary input, IInternalConfiguration config) { input.Upsert("IInternalConfiguration", config); } public static IInternalConfiguration IInternalConfiguration(this IDictionary input) { return input.Get("IInternalConfiguration"); } public static List Errors(this IDictionary input) { var errors = input.Get>("Errors"); return errors ?? new(); } public static DownstreamRouteFinder.DownstreamRouteHolder DownstreamRouteHolder(this IDictionary input) => input.Get("DownstreamRouteHolder"); public static List TemplatePlaceholderNameAndValues(this IDictionary input) => input.Get>("TemplatePlaceholderNameAndValues"); public static DownstreamRequest DownstreamRequest(this IDictionary input) => input.Get("DownstreamRequest"); public static DownstreamResponse DownstreamResponse(this IDictionary input) => input.Get("DownstreamResponse"); public static DownstreamRoute DownstreamRoute(this IDictionary input) => input.Get("DownstreamRoute"); private static T Get(this IDictionary input, string key) => input.TryGetValue(key, out var value) ? (T)value : default; private static void Upsert(this IDictionary input, string key, T value) { if (input.DoesntExist(key)) { input.Add(key, value); } else { input.Remove(key); input.Add(key, value); } } private static bool DoesntExist(this IDictionary input, string key) => !input.ContainsKey(key); } ================================================ FILE: src/Ocelot/Middleware/IBaseUrlFinder.cs ================================================ namespace Ocelot.Middleware; public interface IBaseUrlFinder { string Find(); } ================================================ FILE: src/Ocelot/Middleware/OcelotMiddleware.cs ================================================ using Ocelot.Logging; namespace Ocelot.Middleware; public abstract class OcelotMiddleware { protected OcelotMiddleware(IOcelotLogger logger) { Logger = logger; MiddlewareName = GetType().Name; } public IOcelotLogger Logger { get; } public string MiddlewareName { get; } } ================================================ FILE: src/Ocelot/Middleware/OcelotMiddlewareConfigurationDelegate.cs ================================================ using Microsoft.AspNetCore.Builder; namespace Ocelot.Middleware; public delegate Task OcelotMiddlewareConfigurationDelegate(IApplicationBuilder builder); ================================================ FILE: src/Ocelot/Middleware/OcelotMiddlewareExtensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Ocelot.Administration; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Responses; using System.Diagnostics; namespace Ocelot.Middleware; public static class OcelotMiddlewareExtensions { public static async Task UseOcelot(this IApplicationBuilder builder) { await builder.UseOcelot(new OcelotPipelineConfiguration()); return builder; } public static async Task UseOcelot(this IApplicationBuilder builder, Action pipelineConfiguration) { var config = new OcelotPipelineConfiguration(); pipelineConfiguration?.Invoke(config); return await builder.UseOcelot(config); } public static async Task UseOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) { _ = await CreateConfiguration(builder); ConfigureDiagnosticListener(builder); return CreateOcelotPipeline(builder, pipelineConfiguration); } public static Task UseOcelot(this IApplicationBuilder app, Action builderAction) => UseOcelot(app, builderAction, new OcelotPipelineConfiguration()); public static async Task UseOcelot(this IApplicationBuilder app, Action builderAction, OcelotPipelineConfiguration configuration) { await CreateConfiguration(app); ConfigureDiagnosticListener(app); builderAction?.Invoke(app, configuration ?? new OcelotPipelineConfiguration()); app.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; return app; } private static IApplicationBuilder CreateOcelotPipeline(IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration) { builder.BuildOcelotPipeline(pipelineConfiguration); /* inject first delegate into first piece of asp.net middleware..maybe not like this then because we are updating the http context in ocelot it comes out correct for rest of asp.net.. */ builder.Properties["analysis.NextMiddlewareName"] = "TransitionToOcelotMiddleware"; return builder; } private static async Task CreateConfiguration(IApplicationBuilder builder) { // make configuration from file system? // earlier user needed to add ocelot files in startup configuration stuff, asp.net will map it to this var fileConfig = builder.ApplicationServices.GetService>(); // now create the config var internalConfigCreator = builder.ApplicationServices.GetService(); var internalConfig = await internalConfigCreator.Create(fileConfig.CurrentValue); //Configuration error, throw error message if (internalConfig.IsError) { ThrowToStopOcelotStarting(internalConfig); } // now save it in memory var internalConfigRepo = builder.ApplicationServices.GetService(); internalConfigRepo.AddOrReplace(internalConfig.Data); fileConfig.OnChange(async (config) => { var newInternalConfig = await internalConfigCreator.Create(config); internalConfigRepo.AddOrReplace(newInternalConfig.Data); }); var adminPath = builder.ApplicationServices.GetService(); var configurations = builder.ApplicationServices.GetServices(); // Todo - this has just been added for consul so far...will there be an ordering problem in the future? Should refactor all config into this pattern? foreach (var configuration in configurations) { await configuration(builder); } if (AdministrationApiInUse(adminPath)) { //We have to make sure the file config is set for the ocelot.env.json and ocelot.json so that if we pull it from the //admin api it works...boy this is getting a spit spags boll. var fileConfigSetter = builder.ApplicationServices.GetService(); await SetFileConfig(fileConfigSetter, fileConfig); } return GetOcelotConfigAndReturn(internalConfigRepo); } private static bool AdministrationApiInUse(IAdministrationPath adminPath) { return adminPath != null; } private static async Task SetFileConfig(IFileConfigurationSetter fileConfigSetter, IOptionsMonitor fileConfig) { var response = await fileConfigSetter.Set(fileConfig.CurrentValue); if (IsError(response)) { ThrowToStopOcelotStarting(response); } } private static bool IsError(Response response) { return response == null || response.IsError; } private static IInternalConfiguration GetOcelotConfigAndReturn(IInternalConfigurationRepository provider) { var ocelotConfiguration = provider.Get(); if (ocelotConfiguration?.Data == null || ocelotConfiguration.IsError) { ThrowToStopOcelotStarting(ocelotConfiguration); } return ocelotConfiguration.Data; } private static void ThrowToStopOcelotStarting(Response config) { throw new Exception($"Unable to start Ocelot, errors are:{config.Errors.ToErrorString(true, true)}"); } private static void ConfigureDiagnosticListener(IApplicationBuilder builder) { _ = builder.ApplicationServices.GetService(); var listener = builder.ApplicationServices.GetService(); var diagnosticListener = builder.ApplicationServices.GetService(); diagnosticListener.SubscribeWithAdapter(listener); } } ================================================ FILE: src/Ocelot/Middleware/OcelotPipelineConfiguration.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Ocelot.Middleware; public class OcelotPipelineConfiguration { /// /// This is called after the global error handling middleware so any code before calling next.invoke /// is the next thing called in the Ocelot pipeline. Anything after next.invoke is the last thing called /// in the Ocelot pipeline before we go to the global error handler. /// /// A delegate object. public Func, Task> PreErrorResponderMiddleware { get; set; } /// This allows the user to completely override Ocelot's . /// A delegate object. public Func, Task> ResponderMiddleware { get; set; } /// This is to allow the user to run any extra authentication before the Ocelot authentication kicks in. /// A delegate object. public Func, Task> PreAuthenticationMiddleware { get; set; } /// This allows the user to completely override Ocelot's . /// A delegate object. public Func, Task> AuthenticationMiddleware { get; set; } /// This is to allow the user to run any extra authorization before the Ocelot authorization kicks in. /// A delegate object. public Func, Task> PreAuthorizationMiddleware { get; set; } /// This allows the user to completely override Ocelot's . /// A delegate object. public Func, Task> AuthorizationMiddleware { get; set; } /// This allows the user to completely override Ocelot's . /// A delegate object. public Func, Task> ClaimsToHeadersMiddleware { get; set; } /// This allows the user to implement there own query string manipulation logic. /// A delegate object. public Func, Task> PreQueryStringBuilderMiddleware { get; set; } /// This is an extension that will branch to different pipes. /// A collection. public Dictionary, Action> MapWhenOcelotPipeline { get; } = new(); // TODO fix this data structure } ================================================ FILE: src/Ocelot/Middleware/OcelotPipelineExtensions.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Ocelot.Authentication; using Ocelot.Authorization; using Ocelot.Cache; using Ocelot.Claims.Middleware; using Ocelot.DownstreamPathManipulation.Middleware; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator; using Ocelot.Errors.Middleware; using Ocelot.Headers.Middleware; using Ocelot.LoadBalancer; using Ocelot.Multiplexer; using Ocelot.QueryStrings; using Ocelot.RateLimiting; using Ocelot.Request.Middleware; using Ocelot.Requester.Middleware; using Ocelot.RequestId.Middleware; using Ocelot.Responder.Middleware; using Ocelot.Security.Middleware; using Ocelot.WebSockets; namespace Ocelot.Middleware; public static class OcelotPipelineExtensions { public static RequestDelegate BuildOcelotPipeline(this IApplicationBuilder app, OcelotPipelineConfiguration configuration) { // this sets up the downstream context and gets the config app.UseMiddleware(); // This is registered to catch any global exceptions that are not handled // It also sets the Request Id if anything is set globally app.UseMiddleware(); // If the request is for websockets upgrade we fork into a different pipeline app.MapWhen(httpContext => httpContext.WebSockets.IsWebSocketRequest, ws => { ws.UseMiddleware(); ws.UseMiddleware(); ws.UseMiddleware(); ws.UseMiddleware(); ws.UseMiddleware(); ws.UseMiddleware(); }); // Allow the user to respond with absolutely anything they want. app.UseIfNotNull(configuration.PreErrorResponderMiddleware); // This is registered first so it can catch any errors and issue an appropriate response app.UseIfNotNull(configuration.ResponderMiddleware); // Then we get the downstream route information app.UseMiddleware(); // Multiplex the request if required app.UseMiddleware(); // This security module, IP whitelist blacklist, extended security mechanism app.UseMiddleware(); //Expand other branch pipes if (configuration.MapWhenOcelotPipeline != null) { foreach (var pipeline in configuration.MapWhenOcelotPipeline) { // todo why is this asking for an app app? app.MapWhen(pipeline.Key, pipeline.Value); } } // Now we have the ds route we can transform headers and stuff? app.UseMiddleware(); // Initialises downstream request app.UseMiddleware(); // We check whether the request is ratelimit, and if there is no continue processing app.UseMiddleware(); // This adds or updates the request id (initally we try and set this based on global config in the error handling middleware) // If anything was set at global level and we have a different setting at re route level the global stuff will be overwritten // This means you can get a scenario where you have a different request id from the first piece of middleware to the request id middleware. app.UseMiddleware(); // Allow pre authentication logic. The idea being people might want to run something custom before what is built in. app.UseIfNotNull(configuration.PreAuthenticationMiddleware); // Now we know where the client is going to go we can authenticate them. // We allow the Ocelot middleware to be overriden by whatever the user wants. app.UseIfNotNull(configuration.AuthenticationMiddleware); // The next thing we do is look at any claims transforms in case this is important for authorization app.UseMiddleware(); // Allow pre authorization logic. The idea being people might want to run something custom before what is built in. app.UseIfNotNull(configuration.PreAuthorizationMiddleware); // Now we have authenticated and done any claims transformation, we can authorize the request by AuthorizationMiddleware. // We allow the Ocelot middleware to be overriden by whatever the user wants. app.UseIfNotNull(configuration.AuthorizationMiddleware); // Now we can run the ClaimsToHeadersMiddleware: we allow the Ocelot middleware to be overriden by whatever the user wants. app.UseIfNotNull(configuration.ClaimsToHeadersMiddleware); // Allow the user to implement their own query string manipulation logic app.UseIfNotNull(configuration.PreQueryStringBuilderMiddleware); // Now we can run any claims to query string transformation middleware app.UseMiddleware(); app.UseMiddleware(); // Get the load balancer for this request app.UseMiddleware(); // This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used app.UseMiddleware(); // Not sure if this is the best place for this but we use the downstream url // as the basis for our cache key. app.UseMiddleware(); //We fire off the request and set the response on the scoped data repo app.UseMiddleware(); return app.Build(); } private static IApplicationBuilder UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) => middleware != null ? builder.Use(middleware) : builder; private static IApplicationBuilder UseIfNotNull(this IApplicationBuilder builder, Func, Task> middleware) where TMiddleware : OcelotMiddleware => middleware != null ? builder.Use(middleware) : builder.UseMiddleware(); } ================================================ FILE: src/Ocelot/Middleware/UnauthenticatedError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Middleware; public class UnauthenticatedError : Error { public UnauthenticatedError(string message) : base(message, OcelotErrorCode.UnauthenticatedError, 401) { } } ================================================ FILE: src/Ocelot/Multiplexer/CouldNotFindAggregatorError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Multiplexer; public class CouldNotFindAggregatorError : Error { public CouldNotFindAggregatorError(string aggregator) : base($"Could not find Aggregator: {aggregator}", OcelotErrorCode.CouldNotFindAggregatorError, 404) { } } ================================================ FILE: src/Ocelot/Multiplexer/IDefinedAggregator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Middleware; namespace Ocelot.Multiplexer; public interface IDefinedAggregator { Task Aggregate(List responses); } ================================================ FILE: src/Ocelot/Multiplexer/IDefinedAggregatorProvider.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Multiplexer; public interface IDefinedAggregatorProvider { Response Get(Route route); } ================================================ FILE: src/Ocelot/Multiplexer/IResponseAggregator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; namespace Ocelot.Multiplexer; public interface IResponseAggregator { Task Aggregate(Route route, HttpContext originalContext, List downstreamResponses); } ================================================ FILE: src/Ocelot/Multiplexer/IResponseAggregatorFactory.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Multiplexer; public interface IResponseAggregatorFactory { IResponseAggregator Get(Route route); } ================================================ FILE: src/Ocelot/Multiplexer/InMemoryResponseAggregatorFactory.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Multiplexer; public class InMemoryResponseAggregatorFactory : IResponseAggregatorFactory { private readonly UserDefinedResponseAggregator _userDefined; private readonly IResponseAggregator _simple; public InMemoryResponseAggregatorFactory(IDefinedAggregatorProvider provider, IResponseAggregator responseAggregator) { _userDefined = new UserDefinedResponseAggregator(provider); _simple = responseAggregator; } public IResponseAggregator Get(Route route) => !string.IsNullOrEmpty(route.Aggregator) ? _userDefined : _simple; } ================================================ FILE: src/Ocelot/Multiplexer/MultiplexingMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Newtonsoft.Json.Linq; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using System.Collections; using Route = Ocelot.Configuration.Route; namespace Ocelot.Multiplexer; public class MultiplexingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IResponseAggregatorFactory _factory; private const string RequestIdString = "RequestId"; public MultiplexingMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IResponseAggregatorFactory factory) : base(loggerFactory.CreateLogger()) { _factory = factory; _next = next; } public async Task Invoke(HttpContext httpContext) { var downstreamRouteHolder = httpContext.Items.DownstreamRouteHolder(); var route = downstreamRouteHolder.Route; var downstreamRoutes = route.DownstreamRoute; // Case 1: if websocket request or single downstream route if (ShouldProcessSingleRoute(httpContext, downstreamRoutes)) { await ProcessSingleRouteAsync(httpContext, downstreamRoutes[0]); return; } // Case 2: if no downstream routes if (downstreamRoutes.Count == 0) { return; } // Case 3: if multiple downstream routes var routeKeysConfigs = route.DownstreamRouteConfig; if (routeKeysConfigs == null || routeKeysConfigs.Count == 0) { await ProcessRoutesAsync(httpContext, route); return; } // Case 4: if multiple downstream routes with route keys var mainResponseContext = await ProcessMainRouteAsync(httpContext, downstreamRoutes[0]); if (mainResponseContext == null) { return; } var responsesContexts = await ProcessRoutesWithRouteKeysAsync(httpContext, downstreamRoutes, routeKeysConfigs, mainResponseContext); if (responsesContexts.Length == 0) { return; } await MapResponsesAsync(httpContext, route, mainResponseContext, responsesContexts); } /// /// Helper method to determine if only the first downstream route should be processed. /// It is the case if the request is a websocket request or if there is only one downstream route. /// /// The http context. /// The downstream routes. /// True if only the first downstream route should be processed. private static bool ShouldProcessSingleRoute(HttpContext context, ICollection routes) => context.WebSockets.IsWebSocketRequest || routes.Count == 1; /// /// Processing a single downstream route (no route keys). /// In that case, no need to make copies of the http context. /// /// The http context. /// The downstream route. /// A representing the asynchronous operation. protected virtual Task ProcessSingleRouteAsync(HttpContext context, DownstreamRoute route) { context.Items.UpsertDownstreamRoute(route); return _next.Invoke(context); } /// /// Processing the downstream routes (no route keys). /// /// The main http context. /// The route. private async Task ProcessRoutesAsync(HttpContext context, Route route) { var tasks = route.DownstreamRoute .Select(downstreamRoute => ProcessRouteAsync(context, downstreamRoute)) .ToArray(); var contexts = await Task.WhenAll(tasks); await MapAsync(context, route, new(contexts)); } /// /// When using route keys, the first route is the main route and the rest are additional routes. /// Since we need to break if the main route response is null, we must process the main route first. /// /// The http context. /// The first route, the main route. /// The updated http context. private async Task ProcessMainRouteAsync(HttpContext context, DownstreamRoute route) { context.Items.UpsertDownstreamRoute(route); await _next.Invoke(context); return context; } /// /// Processing the downstream routes with route keys except the main route that has already been processed. /// /// The main http context. /// The downstream routes. /// The route keys config. /// The response from the main route. /// A list of the tasks' http contexts. protected virtual async Task ProcessRoutesWithRouteKeysAsync(HttpContext context, IEnumerable routes, IReadOnlyCollection routeKeysConfigs, HttpContext mainResponse) { var processing = new List>(); var content = await mainResponse.Items.DownstreamResponse().Content.ReadAsStringAsync(); var jObject = JToken.Parse(content); foreach (var downstreamRoute in routes.Skip(1)) { var matchAdvancedAgg = routeKeysConfigs.FirstOrDefault(q => q.RouteKey == downstreamRoute.Key); if (matchAdvancedAgg != null) { processing.AddRange(ProcessRouteWithComplexAggregation(matchAdvancedAgg, jObject, context, downstreamRoute)); continue; } processing.Add(ProcessRouteAsync(context, downstreamRoute)); } return await Task.WhenAll(processing); } /// /// Mapping responses. /// private Task MapResponsesAsync(HttpContext context, Route route, HttpContext mainResponseContext, IEnumerable responsesContexts) { var contexts = new List { mainResponseContext }; contexts.AddRange(responsesContexts); return MapAsync(context, route, contexts); } /// /// Processing a route with aggregation. /// private IEnumerable> ProcessRouteWithComplexAggregation(AggregateRouteConfig matchAdvancedAgg, JToken jObject, HttpContext httpContext, DownstreamRoute downstreamRoute) { var processing = new List>(); var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct(); foreach (var value in values) { var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues(); tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv)); } return processing; } /// /// Process a downstream route asynchronously. /// /// The cloned Http context. private async Task ProcessRouteAsync(HttpContext sourceContext, DownstreamRoute route, List placeholders = null) { var newHttpContext = await CreateThreadContextAsync(sourceContext, route); CopyItemsToNewContext(newHttpContext, sourceContext, placeholders); newHttpContext.Items.UpsertDownstreamRoute(route); await _next.Invoke(newHttpContext); return newHttpContext; } /// /// Copying some needed parameters to the Http context items. /// private static void CopyItemsToNewContext(HttpContext target, HttpContext source, List placeholders = null) { target.Items.Add(RequestIdString, source.Items[RequestIdString]); target.Items.SetIInternalConfiguration(source.Items.IInternalConfiguration()); target.Items.UpsertTemplatePlaceholderNameAndValues(placeholders ?? source.Items.TemplatePlaceholderNameAndValues()); } /// /// Creates a new HttpContext based on the source. /// /// The base http context. /// Downstream route. /// The cloned context. protected virtual async Task CreateThreadContextAsync(HttpContext source, DownstreamRoute route) { var from = source.Request; var bodyStream = await CloneRequestBodyAsync(from, route, source.RequestAborted); var target = new DefaultHttpContext { Request = { Body = bodyStream, ContentLength = from.ContentLength, ContentType = from.ContentType, Host = from.Host, Method = from.Method, Path = from.Path, PathBase = from.PathBase, Protocol = from.Protocol, QueryString = from.QueryString, Scheme = from.Scheme, IsHttps = from.IsHttps, Query = new QueryCollection(new Dictionary(from.Query)), RouteValues = new(from.RouteValues), }, Connection = { RemoteIpAddress = source.Connection.RemoteIpAddress, }, RequestServices = source.RequestServices, RequestAborted = source.RequestAborted, User = source.User, }; foreach (var header in from.Headers) { target.Request.Headers[header.Key] = header.Value.ToArray(); } // Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object return target; } protected virtual Task MapAsync(HttpContext httpContext, Route route, List contexts) { if (route.DownstreamRoute.Count == 1) { return Task.CompletedTask; } var aggregator = _factory.Get(route); return aggregator.Aggregate(route, httpContext, contexts); } protected virtual async Task CloneRequestBodyAsync(HttpRequest request, DownstreamRoute route, CancellationToken aborted) { request.EnableBuffering(); if (request.Body.Position != 0) { Logger.LogWarning(() => $"Ocelot does not support body copy without stream in initial position 0 for the route {route.Name()}."); return request.Body; } var targetBuffer = new MemoryStream(); if (request.ContentLength is not null) { await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted); targetBuffer.Position = 0; request.Body.Position = 0; } else { Logger.LogInformation(() => $"Aggregation does not support body copy without Content-Length header, skipping body copy for the route {route.Name()}."); } return targetBuffer; } } ================================================ FILE: src/Ocelot/Multiplexer/ServiceLocatorDefinedAggregatorProvider.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Multiplexer; public class ServiceLocatorDefinedAggregatorProvider : IDefinedAggregatorProvider { private readonly Dictionary _aggregators; public ServiceLocatorDefinedAggregatorProvider(IServiceProvider services) { _aggregators = services.GetServices().ToDictionary(x => x.GetType().Name); } public Response Get(Route route) { if (_aggregators.TryGetValue(route.Aggregator, out var aggregator)) { return new OkResponse(aggregator); } return new ErrorResponse(new CouldNotFindAggregatorError(route.Aggregator)); } } ================================================ FILE: src/Ocelot/Multiplexer/SimpleJsonResponseAggregator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Middleware; using System.Net.Http.Headers; namespace Ocelot.Multiplexer; public class SimpleJsonResponseAggregator : IResponseAggregator { public async Task Aggregate(Route route, HttpContext originalContext, List downstreamContexts) { await MapAggregateContent(originalContext, downstreamContexts); } private static async Task MapAggregateContent(HttpContext originalContext, List downstreamContexts) { var contentBuilder = new StringBuilder(); contentBuilder.Append('{'); var responseKeys = downstreamContexts.Select(s => s.Items.DownstreamRoute().Key).Distinct().ToArray(); for (var k = 0; k < responseKeys.Length; k++) { var contexts = downstreamContexts.Where(w => w.Items.DownstreamRoute().Key == responseKeys[k]).ToArray(); if (contexts.Length == 1) { if (contexts[0].Items.Errors().Count > 0) { MapAggregateError(originalContext, contexts[0]); return; } var content = await contexts[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); contentBuilder.Append($"\"{responseKeys[k]}\":{content}"); } else { contentBuilder.Append($"\"{responseKeys[k]}\":"); contentBuilder.Append('['); for (var i = 0; i < contexts.Length; i++) { if (contexts[i].Items.Errors().Count > 0) { MapAggregateError(originalContext, contexts[i]); return; } var content = await contexts[i].Items.DownstreamResponse().Content.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(content)) { continue; } contentBuilder.Append($"{content}"); if (i + 1 < contexts.Length) { contentBuilder.Append(','); } } contentBuilder.Append(']'); } if (k + 1 < responseKeys.Length) { contentBuilder.Append(','); } } contentBuilder.Append('}'); var stringContent = new StringContent(contentBuilder.ToString()) { Headers = { ContentType = new MediaTypeHeaderValue("application/json") }, }; originalContext.Items.UpsertDownstreamResponse(new DownstreamResponse(stringContent, HttpStatusCode.OK, new List>>(), "cannot return from aggregate..which reason phrase would you use?")); } private static void MapAggregateError(HttpContext originalContext, HttpContext downstreamContext) { originalContext.Items.UpsertErrors(downstreamContext.Items.Errors()); originalContext.Items.UpsertDownstreamResponse(downstreamContext.Items.DownstreamResponse()); } } ================================================ FILE: src/Ocelot/Multiplexer/UserDefinedResponseAggregator.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Middleware; namespace Ocelot.Multiplexer; public class UserDefinedResponseAggregator : IResponseAggregator { private readonly IDefinedAggregatorProvider _provider; public UserDefinedResponseAggregator(IDefinedAggregatorProvider provider) { _provider = provider; } public async Task Aggregate(Route route, HttpContext originalContext, List downstreamResponses) { var aggregator = _provider.Get(route); if (!aggregator.IsError) { var aggregateResponse = await aggregator.Data .Aggregate(downstreamResponses); originalContext.Items.UpsertDownstreamResponse(aggregateResponse); } else { originalContext.Items.UpsertErrors(aggregator.Errors); } } } ================================================ FILE: src/Ocelot/Ocelot.csproj ================================================  net8.0;net9.0;net10.0 disable disable true Ocelot is an API gateway based on .NET stack. 0.0.0-dev Ocelot Gateway Ocelot API Gateway;.NET Core;.NET https://github.com/ThreeMammals/Ocelot https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png README.md win-x64;osx-x64 false false True false Tom Pallister, Raman Maksimchuk ..\..\codeanalysis.ruleset True 1591 Three Mammals Ocelot Gateway © 2026 Three Mammals. MIT licensed OSS ocelot_icon.png https://github.com/ThreeMammals/Ocelot.git https://github.com/ThreeMammals/Ocelot/blob/main/ReleaseNotes.md LICENSE.md True snupkg full True 8 8 NU1701 ================================================ FILE: src/Ocelot/QualityOfService/IQosFactory.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.QualityOfService; public interface IQoSFactory { Response Get(DownstreamRoute request); } ================================================ FILE: src/Ocelot/QualityOfService/NoQosDelegatingHandler.cs ================================================ namespace Ocelot.QualityOfService; public class NoQosDelegatingHandler : DelegatingHandler { } ================================================ FILE: src/Ocelot/QualityOfService/QosDelegatingHandlerDelegate.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Logging; namespace Ocelot.QualityOfService; public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory); ================================================ FILE: src/Ocelot/QualityOfService/QosFactory.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Responses; namespace Ocelot.QualityOfService; public class QoSFactory : IQoSFactory { private readonly IServiceProvider _serviceProvider; private readonly IOcelotLoggerFactory _ocelotLoggerFactory; private readonly IHttpContextAccessor _contextAccessor; public QoSFactory(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory ocelotLoggerFactory) { _serviceProvider = serviceProvider; _ocelotLoggerFactory = ocelotLoggerFactory; _contextAccessor = contextAccessor; } public Response Get(DownstreamRoute request) { var handler = _serviceProvider.GetService(); if (handler != null) { return new OkResponse(handler(request, _contextAccessor, _ocelotLoggerFactory)); } return new ErrorResponse(new UnableToFindQoSProviderError($"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}")); } } ================================================ FILE: src/Ocelot/QualityOfService/UnableToFindQoSProviderError.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.QualityOfService; public class UnableToFindQoSProviderError : Error { public UnableToFindQoSProviderError(string message) : base(message, OcelotErrorCode.UnableToFindQoSProviderError, StatusCodes.Status404NotFound) { } } ================================================ FILE: src/Ocelot/QueryStrings/AddQueriesToRequest.cs ================================================ using Microsoft.Extensions.Primitives; using Ocelot.Configuration; using Ocelot.Infrastructure.Claims; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.QueryStrings; public class AddQueriesToRequest : IAddQueriesToRequest { private readonly IClaimsParser _claimsParser; public AddQueriesToRequest(IClaimsParser claimsParser) { _claimsParser = claimsParser; } public Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest) { var queryDictionary = ConvertQueryStringToDictionary(downstreamRequest.Query); foreach (var config in claimsToThings) { var value = _claimsParser.GetValue(claims, config.NewKey, config.Delimiter, config.Index); if (value.IsError) { return new ErrorResponse(value.Errors); } var exists = queryDictionary.FirstOrDefault(x => x.Key == config.ExistingKey); if (!string.IsNullOrEmpty(exists.Key)) { queryDictionary[exists.Key] = value.Data; } else { queryDictionary.Add(config.ExistingKey, value.Data); } } downstreamRequest.Query = ConvertDictionaryToQueryString(queryDictionary); return new OkResponse(); } private static Dictionary ConvertQueryStringToDictionary(string queryString) { var query = Microsoft.AspNetCore.WebUtilities.QueryHelpers .ParseQuery(queryString); return query; } private static string ConvertDictionaryToQueryString(Dictionary queryDictionary) { var builder = new StringBuilder(); builder.Append('?'); var outerCount = 0; foreach (var query in queryDictionary) { for (var innerCount = 0; innerCount < query.Value.Count; innerCount++) { builder.Append($"{query.Key}={query.Value[innerCount]}"); if (innerCount < (query.Value.Count - 1)) { builder.Append('&'); } } if (outerCount < (queryDictionary.Count - 1)) { builder.Append('&'); } outerCount++; } return builder.ToString(); } } ================================================ FILE: src/Ocelot/QueryStrings/ClaimsToQueryStringMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.QueryStrings; public class ClaimsToQueryStringMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IAddQueriesToRequest _addQueriesToRequest; public ClaimsToQueryStringMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IAddQueriesToRequest addQueriesToRequest) : base(loggerFactory.CreateLogger()) { _next = next; _addQueriesToRequest = addQueriesToRequest; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); if (downstreamRoute.ClaimsToQueries.Any()) { Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); var downstreamRequest = httpContext.Items.DownstreamRequest(); var response = _addQueriesToRequest.SetQueriesOnDownstreamRequest(downstreamRoute.ClaimsToQueries, httpContext.User.Claims, downstreamRequest); if (response.IsError) { Logger.LogWarning("there was an error setting queries on context, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; } } await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/QueryStrings/IAddQueriesToRequest.cs ================================================ using Ocelot.Configuration; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.QueryStrings; public interface IAddQueriesToRequest { Response SetQueriesOnDownstreamRequest(List claimsToThings, IEnumerable claims, DownstreamRequest downstreamRequest); } ================================================ FILE: src/Ocelot/RateLimiting/ClientRequestIdentity.cs ================================================ namespace Ocelot.RateLimiting; public readonly record struct ClientRequestIdentity(string ClientId, string LoadBalancerKey) { public override string ToString() => $"{ClientId}:{LoadBalancerKey}"; } ================================================ FILE: src/Ocelot/RateLimiting/DistributedCacheRateLimitStorage.cs ================================================ using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; namespace Ocelot.RateLimiting; /// /// Custom storage based on a distributed cache of a remote/local services. /// /// /// See the interface docs for more details. /// public class DistributedCacheRateLimitStorage : IRateLimitStorage { private readonly IDistributedCache _memoryCache; public DistributedCacheRateLimitStorage(IDistributedCache memoryCache) => _memoryCache = memoryCache; public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) => _memoryCache.SetString(id, JsonConvert.SerializeObject(counter), new DistributedCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); public bool Exists(string id) => !string.IsNullOrEmpty(_memoryCache.GetString(id)); public RateLimitCounter? Get(string id) { var stored = _memoryCache.GetString(id); return string.IsNullOrEmpty(stored) ? null : JsonConvert.DeserializeObject(stored); } public void Remove(string id) => _memoryCache.Remove(id); } ================================================ FILE: src/Ocelot/RateLimiting/IRateLimitStorage.cs ================================================ namespace Ocelot.RateLimiting; /// /// Defines a storage for keeping of rate limiting data. /// /// Concrete classes should be based on solutions with excellent performance, such as in-memory solutions. public interface IRateLimitStorage { bool Exists(string id); RateLimitCounter? Get(string id); void Remove(string id); void Set(string id, RateLimitCounter counter, TimeSpan expirationTime); } ================================================ FILE: src/Ocelot/RateLimiting/IRateLimiting.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; namespace Ocelot.RateLimiting; /// /// Defines basic Rate Limiting functionality. /// public interface IRateLimiting { /// Retrieves the key for the attached storage. /// See the interface. /// The current representation of the request. /// The options of rate limiting. /// A value of the key. string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options); /// /// Gets required information to create wanted headers in upper contexts (middleware, etc). /// /// The current context. /// The options of rate limiting. /// The processing moment. /// The counter data. /// A value. RateLimitHeaders GetHeaders(HttpContext context, RateLimitOptions options, DateTime now, RateLimitCounter counter); /// /// Main entry point to process the current request and apply the limiting rule. /// /// Warning! The method performs the storage operations which should be thread safe. /// The representation of current request. /// The current rate limiting options. /// The processing moment. /// A value. RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options, DateTime now); /// /// Counts requests based on the current counter state and taking into account the limiting rule. /// /// Old counter with starting moment inside. /// The limiting rule. /// The processing moment. /// A value. RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule, DateTime now); /// /// Gets the seconds to wait for the next retry by starting moment and the rule. /// /// The method must be called after the counting by the method is completed; otherwise it doesn't make sense. /// The counter with starting moment inside. /// The limiting rule. /// The processing moment. /// A value in seconds. double RetryAfter(RateLimitCounter counter, RateLimitRule rule, DateTime now); /// /// Converts to time span from a string, such as "1ms", "1s", "1m", "1h", "1d". /// /// The string value with units: '1ms', '1s', '1m', '1h', '1d'. /// A value. TimeSpan ToTimespan(string timespan); } ================================================ FILE: src/Ocelot/RateLimiting/MemoryCacheRateLimitStorage.cs ================================================ using Microsoft.Extensions.Caching.Memory; namespace Ocelot.RateLimiting; /// /// Default storage based on the memory cache of the local web server instance. /// /// /// See the interface docs for more details. /// public class MemoryCacheRateLimitStorage : IRateLimitStorage { private readonly IMemoryCache _memoryCache; public MemoryCacheRateLimitStorage(IMemoryCache memoryCache) => _memoryCache = memoryCache; public void Set(string id, RateLimitCounter counter, TimeSpan expirationTime) => _memoryCache.Set(id, counter, new MemoryCacheEntryOptions().SetAbsoluteExpiration(expirationTime)); public bool Exists(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter); public RateLimitCounter? Get(string id) => _memoryCache.TryGetValue(id, out RateLimitCounter counter) ? counter : null; public void Remove(string id) => _memoryCache.Remove(id); } ================================================ FILE: src/Ocelot/RateLimiting/QuotaExceededError.cs ================================================ using Ocelot.Errors; namespace Ocelot.RateLimiting; public class QuotaExceededError : Error { public QuotaExceededError(string message, int httpStatusCode) : base(message, OcelotErrorCode.QuotaExceededError, httpStatusCode) { } } ================================================ FILE: src/Ocelot/RateLimiting/RateLimitCounter.cs ================================================ using System.Globalization; using System.Text.Json.Serialization; using NewtonsoftJsonConstructor = Newtonsoft.Json.JsonConstructorAttribute; namespace Ocelot.RateLimiting; /// /// Stores the initial access time and the numbers of calls made from that point. /// public struct RateLimitCounter { public RateLimitCounter(DateTime startedAt) { StartedAt = startedAt; Total = 1; } [JsonConstructor] [NewtonsoftJsonConstructor] public RateLimitCounter(DateTime startedAt, DateTime? exceededAt, long totalRequests) { StartedAt = startedAt; ExceededAt = exceededAt; Total = totalRequests; } /// The moment when the counting was started. /// A value of the moment. public DateTime StartedAt { get; } /// The moment when the limit was exceeded. /// A value of the moment. public DateTime? ExceededAt; /// Total number of requests counted. /// A value of total number. public long Total; public override readonly string ToString() { string started = StartedAt.ToString("O", CultureInfo.InvariantCulture); string exceeded = ExceededAt.HasValue ? $"+{ExceededAt.Value - StartedAt}" : string.Empty; return $"{Total}->({started}){exceeded}"; } } ================================================ FILE: src/Ocelot/RateLimiting/RateLimitHeaders.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.RateLimiting; public class RateLimitHeaders { protected RateLimitHeaders() { } public RateLimitHeaders(HttpContext context, long limit, long remaining, DateTime reset) { Context = context; Limit = limit; Remaining = remaining; Reset = reset; } /// /// Original context. /// /// An object. public HttpContext Context { get; } /// /// Total number of requests allowed in the current time window. /// /// An value. public long Limit { get; } /// /// Number of requests remaining before hitting the limit. /// /// An value. public long Remaining { get; } /// /// Timestamp when the rate limit window resets. /// /// A value. public DateTime Reset { get; } public override string ToString() => $"{Remaining}/{Limit} resets at {Reset:O}"; } ================================================ FILE: src/Ocelot/RateLimiting/RateLimiting.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; using Ocelot.Configuration; using Ocelot.Infrastructure.Extensions; using System.Security.Cryptography; namespace Ocelot.RateLimiting; public class RateLimiting : IRateLimiting { private readonly IRateLimitStorage _storage; #pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable IDE0330 // Prefer 'System.Threading.Lock' private static readonly object ProcessLocker = new(); private static readonly TimeSpan OneSecond = TimeSpan.FromSeconds(1); public RateLimiting(IRateLimitStorage storage) { _storage = storage; } /// /// Main entry point to process the current request and apply the limiting rule. /// /// Warning! The method performs the storage operations which MUST BE thread safe. /// The representation of current request. /// The current rate limiting options. /// The processing moment. /// A value. public virtual RateLimitCounter ProcessRequest(ClientRequestIdentity identity, RateLimitOptions options, DateTime now) { RateLimitCounter counter; var rule = options.Rule; var counterId = GetStorageKey(identity, options); // Serial reads/writes from/to the storage which must be thread safe lock (ProcessLocker) { var entry = _storage.Get(counterId); counter = Count(entry, rule, now); if (counter.Total > rule.Limit) { var retryAfter = RetryAfter(counter, rule, now); // the calculation depends on the counter returned from Count() method if (retryAfter < 0) { // Wait window period elapsed, reset counter, and start the next counting period counter = new RateLimitCounter(now); } } // TODO: The expiry approach doesn't make much sense in practice because // if the counting period elapses or there are prolonged pending periods, // the counter resets to a state of 1, with null values after expiry treated as a count of 1. // It might make sense to consider request timeout periods as expiry periods. var expiration = rule.PeriodSpan + rule.WaitSpan; // absolute max period of processing expiration += OneSecond; // add an extra second as a shift to allow to synchronize concurrent threads _storage.Set(counterId, counter, expiration); } return counter; } /// /// Counts requests based on the current counter state and taking into account the limiting rule. /// /// Old counter with starting moment inside. /// The limiting rule. /// The processing moment. /// A value. public virtual RateLimitCounter Count(RateLimitCounter? entry, RateLimitRule rule, DateTime now) { if (!entry.HasValue) { return new RateLimitCounter(now); // no entry, start counting, and the current request is the 1st one } var counter = entry.Value; if (++counter.Total > rule.Limit && !counter.ExceededAt.HasValue) // current request exceeds the limit { counter.ExceededAt = now; // the exceeding moment is now, this request should fail } bool isInFixedWindow = counter.StartedAt + rule.PeriodSpan >= now; // the fixed window counting period bool isInWaitWindow = counter.ExceededAt.HasValue && counter.ExceededAt.Value + rule.WaitSpan >= now; // with including equality, treating the end of waiting as the end of the wait window return isInFixedWindow || isInWaitWindow ? counter // still count : new RateLimitCounter(now); // Wait window period elapsed, start counting NOW! } public virtual RateLimitHeaders GetHeaders(HttpContext context, RateLimitOptions options, DateTime now, RateLimitCounter counter) { var rule = options.Rule; return new RateLimitHeaders(context, limit: rule.Limit, remaining: rule.Limit - counter.Total, reset: counter.StartedAt + rule.PeriodSpan); } /// /// Gets the SHA1-hashed value of a unique key for caching, using the service through the service. /// /// Notes: /// The generated identity key includes the as a prefix to ensure it is recognized in distributed storage systems, like services, aiding users in observing/managing cached objects. /// By default, each Ocelot instance employs its own service, without synchronization across instances. /// /// Specifies the client's identity. /// Defines the current route rate-limiting options. /// Returns a SHA1-hashed object as the caching key. public virtual string GetStorageKey(ClientRequestIdentity identity, RateLimitOptions options) { var key = $"{options.KeyPrefix}_{identity}_{options.Rule}"; var idBytes = Encoding.UTF8.GetBytes(key); var hashBytes = SHA1.HashData(idBytes); return Convert.ToHexString(hashBytes); } /// /// Gets the seconds to wait for the next retry by starting moment and the rule. /// /// The method must be called after the one. /// The counter state. /// The current rule. /// The processing moment. /// An value of seconds. public virtual double RetryAfter(RateLimitCounter counter, RateLimitRule rule, DateTime now) { if (counter.Total <= rule.Limit || !counter.ExceededAt.HasValue) { return 0.0D; // happy path, no need to retry, current request is valid, continue counting } // Counting Period is active bool doNotWait = rule.WaitSpan == TimeSpan.Zero || rule.Wait.IsEmpty() || rule.Wait == RateLimitRule.ZeroWait; if (doNotWait && counter.StartedAt + rule.PeriodSpan > now) { //return waitWindow.TotalSeconds - (now - exceededAt).TotalSeconds; // minus seconds past var retryAfter = counter.StartedAt + rule.PeriodSpan - now; return retryAfter.TotalSeconds; // positive value of seconds until the end of the sliding period in fixed window } // Exceeding was happen && Wait period is active (no sliding) var waitWindow = rule.WaitSpan; // good non-zero value var exceededAt = counter.ExceededAt.Value; if (exceededAt + waitWindow > now) { var retryAfter = exceededAt + waitWindow - now; return retryAfter.TotalSeconds; // positive value of seconds until the end of the waiting period } return -1.0D; // counting period vs wait period elapsed, no need to retry, reset the counter in upper calling context } /// /// Converts to time span from a string, such as "1ms", "1s", "1m", "1h", "1d". /// /// The string value with units: '1ms', '1s', '1m', '1h', '1d'. /// A value. /// See more in the method docs. public virtual TimeSpan ToTimespan(string timespan) => RateLimitRule.ParseTimespan(timespan); } ================================================ FILE: src/Ocelot/RateLimiting/RateLimitingHeaders.cs ================================================ using Microsoft.Net.Http.Headers; namespace Ocelot.RateLimiting; /// /// TODO These Ocelot's RateLimiting headers don't follow industry standards, see links. /// /// Links: /// /// GitHub: draft-polli-ratelimit-headers /// GitHub: ratelimit-headers /// GitHub Wiki: RateLimit header fields for HTTP /// StackOverflow: Examples of HTTP API Rate Limiting HTTP Response headers /// /// public static class RateLimitingHeaders { public const char Dash = '-'; public const char Underscore = '_'; /// Gets the Retry-After HTTP header name. public static readonly string Retry_After = HeaderNames.RetryAfter; /// Gets the X-RateLimit-Limit Ocelot's header name. public static readonly string X_RateLimit_Limit = nameof(X_RateLimit_Limit).Replace(Underscore, Dash); /// Gets the X-RateLimit-Remaining Ocelot's header name. public static readonly string X_RateLimit_Remaining = nameof(X_RateLimit_Remaining).Replace(Underscore, Dash); /// Gets the X-RateLimit-Reset Ocelot's header name. public static readonly string X_RateLimit_Reset = nameof(X_RateLimit_Reset).Replace(Underscore, Dash); } ================================================ FILE: src/Ocelot/RateLimiting/RateLimitingMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Ocelot.Configuration; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; using System.Globalization; namespace Ocelot.RateLimiting; public class RateLimitingMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IRateLimiting _limiter; private readonly IHttpContextAccessor _contextAccessor; public RateLimitingMiddleware( RequestDelegate next, IOcelotLoggerFactory factory, IRateLimiting limiter, IHttpContextAccessor contextAccessor) : base(factory.CreateLogger()) { _next = next; _limiter = limiter; _contextAccessor = contextAccessor; } public Task Invoke(HttpContext context) { var now = DateTime.UtcNow; context.Items.Add(nameof(DateTime.UtcNow), now); var downstreamRoute = context.Items.DownstreamRoute(); var options = downstreamRoute.RateLimitOptions ?? new(false); if (!options.EnableRateLimiting) { Logger.LogInformation(() => $"Rate limiting is disabled for route '{downstreamRoute.Name()}' via the {nameof(RateLimitOptions.EnableRateLimiting)} option."); return _next.Invoke(context); } var identity = Identify(context, options, downstreamRoute); if (IsWhitelisted(identity, options)) { Logger.LogInformation(() => $"Route '{downstreamRoute.Name()}' is configured to bypass rate limiting based on the client's header, due to the client's ID being detected in the whitelist."); return _next.Invoke(context); } // Log warnings and break execution var rule = options.Rule; var warning = string.Empty; if (identity.ClientId.IsEmpty()) // unknown client aka security check, so block unknown clients { warning = $"Rate limiting client could not be identified for the route '{downstreamRoute.Name(true)}' due to a missing or unknown client ID header required by rule '{rule}'!"; // and don't log the header name because of security } if (!warning.IsEmpty()) { Logger.LogWarning(warning); RateLimitOptions errorOpts = new(options) { QuotaMessage = warning, StatusCode = StatusCodes.Status503ServiceUnavailable, }; return Break(context, errorOpts, -1.0); } var counter = _limiter.ProcessRequest(identity, options, now); if (counter.Total > rule.Limit) { var retryAfter = _limiter.RetryAfter(counter, rule, now); // compute retry after value based on counter state LogBlockedRequest(context, identity, counter, options, downstreamRoute); // log blocked request virtually return Break(context, options, retryAfter); } // Set X-RateLimit-* headers for the longest period var originalContext = _contextAccessor.HttpContext; if (options.EnableHeaders && originalContext != null) { var headers = _limiter.GetHeaders(originalContext, options, now, counter); originalContext.Response.OnStarting(SetRateLimitHeaders, state: headers); Logger.LogInformation(() => $"Route '{downstreamRoute.Name()}' must return rate limiting headers with the following data: {headers}"); } return _next.Invoke(context); } protected virtual Task Break(HttpContext context, RateLimitOptions options, double retryAfter) { var retryAfterHeader = retryAfter.ToString(CultureInfo.InvariantCulture); var ds = ReturnQuotaExceededResponse(context, options, retryAfterHeader); context.Items.UpsertDownstreamResponse(ds); var error = new QuotaExceededError(GetResponseMessage(options), options.StatusCode); context.Items.SetError(error); return Task.CompletedTask; } protected virtual ClientRequestIdentity Identify(HttpContext context, RateLimitOptions options, DownstreamRoute route) { var clientId = string.Empty; var header = options.ClientIdHeader.IfEmpty(RateLimitOptions.DefaultClientHeader); if (context.Request.Headers.TryGetValue(header, out var headerValue)) { clientId = headerValue; } return new ClientRequestIdentity(clientId, route.LoadBalancerKey); } public static bool IsWhitelisted(ClientRequestIdentity identity, RateLimitOptions options) => options.ClientWhitelist.Contains(identity.ClientId); public virtual void LogBlockedRequest(HttpContext context, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitOptions options, DownstreamRoute route) { var req = context.Request; var rule = options.Rule; Logger.LogWarning(() => $"Blocked request: {req.Method} {req.Path} from client with {options.ClientIdHeader}({identity.ClientId}) header, quota {rule.Limit}/{rule.Period} exceeded by {counter.Total}. Blocked by rate limiting rule '{rule}' of the route '{route.Name()}' with {nameof(HttpContext.TraceIdentifier)}:{context.TraceIdentifier}."); } public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext context, RateLimitOptions options, string retryAfter) { var message = GetResponseMessage(options); var http = new HttpResponseMessage((HttpStatusCode)options.StatusCode) { Content = new StringContent(message), }; if (options.EnableHeaders) { http.Headers.TryAddWithoutValidation(HeaderNames.RetryAfter, retryAfter); // in seconds, not date string context.Response.Headers.RetryAfter = retryAfter; } return new DownstreamResponse(http); } protected virtual string GetResponseMessage(RateLimitOptions options) { var format = options.QuotaMessage.IfEmpty(RateLimitOptions.DefaultQuotaMessage); return string.Format(format, options.Rule.Limit, options.Rule.Period); } /// TODO: Produced Ocelot's headers don't follow industry standards. /// More details in docs. /// Captured state as a object. /// A object. protected virtual Task SetRateLimitHeaders(object state) { var limitHeaders = (RateLimitHeaders)state; var headers = limitHeaders.Context.Response.Headers; headers[RateLimitingHeaders.X_RateLimit_Limit] = new StringValues(limitHeaders.Limit.ToString()); headers[RateLimitingHeaders.X_RateLimit_Remaining] = new StringValues(limitHeaders.Remaining.ToString()); headers[RateLimitingHeaders.X_RateLimit_Reset] = new StringValues(limitHeaders.Reset.ToUniversalTime().ToString("o", DateTimeFormatInfo.InvariantInfo)); return Task.CompletedTask; } } ================================================ FILE: src/Ocelot/Request/Creator/DownstreamRequestCreator.cs ================================================ using Ocelot.Infrastructure; using Ocelot.Request.Middleware; namespace Ocelot.Request.Creator; public class DownstreamRequestCreator : IDownstreamRequestCreator { private readonly IFrameworkDescription _framework; private const string DotNetFramework = ".NET Framework"; public DownstreamRequestCreator(IFrameworkDescription framework) { _framework = framework; } /// /// According to https://tools.ietf.org/html/rfc7231 /// GET,HEAD,DELETE,CONNECT,TRACE /// Can have body but server can reject the request. /// And MS HttpClient in Full Framework actually rejects it. /// See #366 issue. /// /// The HTTP request. /// A object. public DownstreamRequest Create(HttpRequestMessage request) { if (_framework.Get().Contains(DotNetFramework)) { if (request.Method == HttpMethod.Get || request.Method == HttpMethod.Head || request.Method == HttpMethod.Delete || request.Method == HttpMethod.Trace) { request.Content = null; } } return new DownstreamRequest(request); } } ================================================ FILE: src/Ocelot/Request/Creator/IDownstreamRequestCreator.cs ================================================ using Ocelot.Request.Middleware; namespace Ocelot.Request.Creator; public interface IDownstreamRequestCreator { DownstreamRequest Create(HttpRequestMessage request); } ================================================ FILE: src/Ocelot/Request/Mapper/IRequestMapper.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; namespace Ocelot.Request.Mapper; public interface IRequestMapper { HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute); } ================================================ FILE: src/Ocelot/Request/Mapper/PayloadTooLargeError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Request.Mapper; public class PayloadTooLargeError : Error { public PayloadTooLargeError(Exception exception) : base(exception.Message, OcelotErrorCode.PayloadTooLargeError, (int) System.Net.HttpStatusCode.RequestEntityTooLarge) { } } ================================================ FILE: src/Ocelot/Request/Mapper/RequestMapper.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Primitives; using Ocelot.Configuration; namespace Ocelot.Request.Mapper; public class RequestMapper : IRequestMapper { private static readonly HashSet UnsupportedHeaders = new(StringComparer.OrdinalIgnoreCase) { "host", "transfer-encoding" }; private static readonly string[] ContentHeaders = { "Content-Length", "Content-Language", "Content-Location", "Content-Range", "Content-MD5", "Content-Disposition", "Content-Encoding" }; public HttpRequestMessage Map(HttpRequest request, DownstreamRoute downstreamRoute) { var requestMessage = new HttpRequestMessage { Content = MapContent(request), Method = MapMethod(request, downstreamRoute), RequestUri = MapUri(request), Version = downstreamRoute.DownstreamHttpVersion, VersionPolicy = downstreamRoute.DownstreamHttpVersionPolicy, }; MapHeaders(request, requestMessage); return requestMessage; } private static HttpContent MapContent(HttpRequest request) { HttpContent content; // No content if we have no body or if the request has no content according to RFC 2616 section 4.3 if (request.Body == null || (!request.ContentLength.HasValue && StringValues.IsNullOrEmpty(request.Headers.TransferEncoding))) { return null; } content = request.ContentLength is 0 ? new ByteArrayContent(Array.Empty()) : new StreamHttpContent(request.HttpContext); AddContentHeaders(request, content); return content; } private static void AddContentHeaders(HttpRequest request, HttpContent content) { if (!string.IsNullOrEmpty(request.ContentType)) { content.Headers .TryAddWithoutValidation("Content-Type", new[] { request.ContentType }); } // The performance might be improved by retrieving the matching headers from the request // instead of calling request.Headers.TryGetValue for each used content header var matchingHeaders = ContentHeaders.Where(request.Headers.ContainsKey); foreach (var key in matchingHeaders) { if (!request.Headers.TryGetValue(key, out var value)) { continue; } content.Headers.TryAddWithoutValidation(key, value.ToArray()); } } private static HttpMethod MapMethod(HttpRequest request, DownstreamRoute downstreamRoute) => !string.IsNullOrEmpty(downstreamRoute?.DownstreamHttpMethod) ? new HttpMethod(downstreamRoute.DownstreamHttpMethod) : new HttpMethod(request.Method); // TODO Review this method, request.GetEncodedUrl() could throw a NullReferenceException private static Uri MapUri(HttpRequest request) => new(request.GetEncodedUrl()); private static void MapHeaders(HttpRequest request, HttpRequestMessage requestMessage) { foreach (var header in request.Headers) { if (IsSupportedHeader(header)) { requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()); } } } private static bool IsSupportedHeader(KeyValuePair header) => !UnsupportedHeaders.Contains(header.Key); } ================================================ FILE: src/Ocelot/Request/Mapper/StreamHttpContent.cs ================================================ using Microsoft.AspNetCore.Http; using System.Buffers; namespace Ocelot.Request.Mapper; public class StreamHttpContent : HttpContent { private const int DefaultBufferSize = 65536; public const long UnknownLength = -1; private readonly HttpContext _context; private readonly long _contentLength; public StreamHttpContent(HttpContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); _contentLength = context.Request.ContentLength ?? UnknownLength; } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken) => CopyAsync(_context.Request.Body, stream, _contentLength, false, cancellationToken); protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => CopyAsync(_context.Request.Body, stream, _contentLength, false, CancellationToken.None); protected override bool TryComputeLength(out long length) { length = _contentLength; return length >= 0; } // This is used internally by HttpContent.ReadAsStreamAsync(...) protected override Task CreateContentReadStreamAsync() { // Nobody should be calling this... throw new NotImplementedException(); } private static async Task CopyAsync(Stream input, Stream output, long announcedContentLength, bool autoFlush, CancellationToken cancellation) { // For smaller payloads, avoid allocating a buffer that is larger than the announced content length var minBufferSize = announcedContentLength != UnknownLength && announcedContentLength < DefaultBufferSize ? (int)announcedContentLength : DefaultBufferSize; var buffer = ArrayPool.Shared.Rent(minBufferSize); long contentLength = 0; try { while (true) { // Issue a zero-byte read to the input stream to defer buffer allocation until data is available. // Note that if the underlying stream does not supporting blocking on zero byte reads, then this will // complete immediately and won't save any memory, but will still function correctly. var zeroByteReadTask = input.ReadAsync(Memory.Empty, cancellation); if (zeroByteReadTask.IsCompletedSuccessfully) { // Consume the ValueTask's result in case it is backed by an IValueTaskSource // It is save to read the Result once after the ValueTask has completed, and we've checked for complition by IsCompletedSuccessfully property // See remarks: https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask-1.result?view=net-8.0#remarks _ = zeroByteReadTask.Result; // No need to await the task by .GetAwaiter().GetResult() } else { // Take care not to return the same buffer to the pool twice in case zeroByteReadTask throws var bufferToReturn = buffer; buffer = null; ArrayPool.Shared.Return(bufferToReturn); await zeroByteReadTask; buffer = ArrayPool.Shared.Rent(minBufferSize); } var read = await input.ReadAsync(buffer.AsMemory(), cancellation); contentLength += read; // Normally this is enforced by the server, but it could get out of sync if something in the proxy modified the body. if (announcedContentLength != UnknownLength && contentLength > announcedContentLength) { throw new InvalidOperationException($"More data ({contentLength} bytes) received than the specified Content-Length of {announcedContentLength} bytes."); } // End of the source stream. if (read == 0) { if (announcedContentLength == UnknownLength || contentLength == announcedContentLength) { return; } else { throw new InvalidOperationException($"Sent {contentLength} request content bytes, but Content-Length promised {announcedContentLength}."); } } await output.WriteAsync(buffer.AsMemory(0, read), cancellation); if (autoFlush) { // HttpClient doesn't always flush outgoing data unless the buffer is full or the caller asks. // This is a problem for streaming protocols like WebSockets and gRPC. await output.FlushAsync(cancellation); } } } finally { if (buffer != null) { ArrayPool.Shared.Return(buffer); } } } } ================================================ FILE: src/Ocelot/Request/Mapper/UnmappableRequestError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Request.Mapper; public class UnmappableRequestError : Error { public UnmappableRequestError(Exception exception) : base($"Error when parsing incoming request, exception: {exception}", OcelotErrorCode.UnmappableRequestError, 404) { } } ================================================ FILE: src/Ocelot/Request/Middleware/DownstreamRequest.cs ================================================ using System.Net.Http.Headers; namespace Ocelot.Request.Middleware; public class DownstreamRequest { private readonly HttpRequestMessage _request; public DownstreamRequest() { } public DownstreamRequest(HttpRequestMessage request) { _request = request; Method = _request.Method.Method; OriginalString = _request.RequestUri.OriginalString; Scheme = _request.RequestUri.Scheme; Host = _request.RequestUri.Host; Port = _request.RequestUri.Port; AbsolutePath = _request.RequestUri.AbsolutePath; Query = _request.RequestUri.Query; } public HttpHeaders Headers { get => _request.Headers; } public string Method { get; } public string OriginalString { get; } public string Scheme { get; set; } public string Host { get; set; } public int Port { get; set; } public string AbsolutePath { get; set; } public string Query { get; set; } public bool HasContent { get => _request?.Content != null; } public HttpRequestMessage Request { get => _request; } public HttpRequestMessage ToHttpRequestMessage() { var uriBuilder = new UriBuilder { Port = Port, Host = Host, Path = AbsolutePath, Query = RemoveLeadingQuestionMark(Query), Scheme = Scheme, }; _request.RequestUri = uriBuilder.Uri; _request.Method = new HttpMethod(Method); return _request; } public string ToUri() { var uriBuilder = new UriBuilder { Port = Port, Host = Host, Path = AbsolutePath, Query = RemoveLeadingQuestionMark(Query), Scheme = Scheme, }; return uriBuilder.Uri.AbsoluteUri; } public override string ToString() { return ToUri(); } private static string RemoveLeadingQuestionMark(string query) { if (!string.IsNullOrEmpty(query) && query.StartsWith('?')) { return query.Substring(1); } return query; } } ================================================ FILE: src/Ocelot/Request/Middleware/DownstreamRequestInitialiserMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; namespace Ocelot.Request.Middleware; public class DownstreamRequestInitialiserMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IRequestMapper _requestMapper; private readonly IDownstreamRequestCreator _creator; public DownstreamRequestInitialiserMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestMapper requestMapper, IDownstreamRequestCreator creator) : base(loggerFactory.CreateLogger()) { _next = next; _requestMapper = requestMapper; _creator = creator; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); HttpRequestMessage httpRequestMessage; try { httpRequestMessage = _requestMapper.Map(httpContext.Request, downstreamRoute); } catch (Exception ex) { // TODO Review the error handling, we should throw an exception here and use the global error handler middleware to catch it httpContext.Items.SetError(new UnmappableRequestError(ex)); return; } var downstreamRequest = _creator.Create(httpRequestMessage); httpContext.Items.UpsertDownstreamRequest(downstreamRequest); await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/RequestId/DefaultRequestIdKey.cs ================================================ namespace Ocelot.RequestId; public static class DefaultRequestIdKey { // This is set incase anyone isnt doing this specifically with there requests. // It will not be forwarded on to downstream services unless specfied in the config. public const string Value = "RequestId"; } ================================================ FILE: src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using System.Net.Http.Headers; namespace Ocelot.RequestId.Middleware; public class RequestIdMiddleware : OcelotMiddleware { public const string RequestIdName = nameof(IInternalConfiguration.RequestId); public const string PreviousRequestIdName = "Previous" + nameof(IInternalConfiguration.RequestId); private readonly RequestDelegate _next; private readonly IRequestScopedDataRepository _requestScopedDataRepository; public RequestIdMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository) : base(loggerFactory.CreateLogger()) { _next = next; _requestScopedDataRepository = requestScopedDataRepository; } public async Task Invoke(HttpContext httpContext) { SetOcelotRequestId(httpContext); await _next.Invoke(httpContext); } private void SetOcelotRequestId(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); var key = downstreamRoute.RequestIdKey ?? DefaultRequestIdKey.Value; if (httpContext.Request.Headers.TryGetValue(key, out var upstreamRequestIds)) { httpContext.TraceIdentifier = upstreamRequestIds.First(); var previousRequestId = _requestScopedDataRepository.Get(RequestIdName); if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != httpContext.TraceIdentifier) { _requestScopedDataRepository.Add(PreviousRequestIdName, previousRequestId.Data); _requestScopedDataRepository.Update(RequestIdName, httpContext.TraceIdentifier); } else { _requestScopedDataRepository.Add(RequestIdName, httpContext.TraceIdentifier); } } var requestId = new RequestId(downstreamRoute.RequestIdKey, httpContext.TraceIdentifier); var downstreamRequest = httpContext.Items.DownstreamRequest(); if (ShouldAddRequestId(requestId, downstreamRequest.Headers)) { AddRequestIdHeader(requestId, downstreamRequest); } } private static bool ShouldAddRequestId(RequestId requestId, HttpHeaders headers) { return !string.IsNullOrEmpty(requestId?.RequestIdKey) && !string.IsNullOrEmpty(requestId.RequestIdValue) && !RequestIdInHeaders(requestId, headers); } private static bool RequestIdInHeaders(RequestId requestId, HttpHeaders headers) { return headers.TryGetValues(requestId.RequestIdKey, out var value); } private static void AddRequestIdHeader(RequestId requestId, DownstreamRequest httpRequestMessage) { httpRequestMessage.Headers.Add(requestId.RequestIdKey, requestId.RequestIdValue); } } ================================================ FILE: src/Ocelot/RequestId/RequestId.cs ================================================ namespace Ocelot.RequestId; public class RequestId { public RequestId(string requestIdKey, string requestIdValue) { RequestIdKey = requestIdKey; RequestIdValue = requestIdValue; } public string RequestIdKey { get; } public string RequestIdValue { get; } } ================================================ FILE: src/Ocelot/Requester/ConnectionToDownstreamServiceError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Requester; public class ConnectionToDownstreamServiceError : Error { public ConnectionToDownstreamServiceError(Exception exception) : base($"Error connecting to downstream service, exception: {exception}", OcelotErrorCode.ConnectionToDownstreamServiceError, 502) { } } ================================================ FILE: src/Ocelot/Requester/DelegatingHandlerFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.QualityOfService; namespace Ocelot.Requester; public class DelegatingHandlerFactory : IDelegatingHandlerFactory { private readonly ITracingHandlerFactory _tracingFactory; private readonly IQoSFactory _qoSFactory; private readonly IServiceProvider _serviceProvider; private readonly IOcelotLogger _logger; public DelegatingHandlerFactory( ITracingHandlerFactory tracingFactory, IQoSFactory qoSFactory, IServiceProvider serviceProvider, IOcelotLoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); _serviceProvider = serviceProvider; _tracingFactory = tracingFactory; _qoSFactory = qoSFactory; } public List Get(DownstreamRoute route) { var globalDelegatingHandlers = _serviceProvider.GetServices() .ToArray(); var routeSpecificHandlers = _serviceProvider.GetServices() .ToList(); var handlers = new List(); foreach (var handler in globalDelegatingHandlers) { if (GlobalIsInHandlersConfig(route, handler)) { routeSpecificHandlers.Add(handler.DelegatingHandler); } else { handlers.Add(handler.DelegatingHandler); } } if (route.DelegatingHandlers.Count != 0) { var sorted = SortByConfigOrder(route, routeSpecificHandlers); handlers.AddRange(sorted); } if (route.HttpHandlerOptions.UseTracing) { handlers.Add((DelegatingHandler)_tracingFactory.Get()); } if (route.QosOptions.UseQos) { var handler = _qoSFactory.Get(route); if (handler?.IsError == false) { handlers.Add(handler.Data); } else { _logger.LogWarning(() => $"Route '{route.Name()}' specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"); handlers.Add(new NoQosDelegatingHandler()); } } return handlers; } private static DelegatingHandler[] SortByConfigOrder(DownstreamRoute request, List routeSpecificHandlers) { return routeSpecificHandlers .Where(x => request.DelegatingHandlers.Contains(x.GetType().Name)) .OrderBy(d => { var type = d.GetType().Name; var pos = request.DelegatingHandlers.IndexOf(type); return pos; }).ToArray(); } private static bool GlobalIsInHandlersConfig(DownstreamRoute request, GlobalDelegatingHandler handler) => request.DelegatingHandlers.Contains(handler.DelegatingHandler.GetType().Name); } ================================================ FILE: src/Ocelot/Requester/GlobalDelegatingHandler.cs ================================================ namespace Ocelot.Requester; public class GlobalDelegatingHandler { public GlobalDelegatingHandler(DelegatingHandler delegatingHandler) { DelegatingHandler = delegatingHandler; } public DelegatingHandler DelegatingHandler { get; } } ================================================ FILE: src/Ocelot/Requester/HttpExceptionToErrorMapper.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Errors; using Ocelot.Request.Mapper; namespace Ocelot.Requester; public class HttpExceptionToErrorMapper : IExceptionToErrorMapper { /// This is a dictionary of custom mappers for exceptions. private readonly IDictionary> _mappers; public HttpExceptionToErrorMapper(IServiceProvider serviceProvider) { _mappers = serviceProvider.GetService>>(); } public Error Map(Exception exception) { var type = exception.GetType(); // If there is a custom mapper for this exception type, use it // The idea is the following: When implementing features or providers, // you can provide a custom mapper if (_mappers != null && _mappers.TryGetValue(type, out var mapper)) { return mapper(exception); } // here are mapped the exceptions thrown from Ocelot core application if (type == typeof(TimeoutException)) { return new RequestTimedOutError(exception); } if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException))) { return new RequestCanceledError(exception.Message); } if (type == typeof(HttpRequestException) || type == typeof(TimeoutException)) { // Inner exception is a BadHttpRequestException, and only this exception exposes the StatusCode property. // We check if the inner exception is a BadHttpRequestException and if the StatusCode is 413, we return a PayloadTooLargeError if (exception.InnerException is BadHttpRequestException { StatusCode: StatusCodes.Status413RequestEntityTooLarge }) { return new PayloadTooLargeError(exception); } return new ConnectionToDownstreamServiceError(exception); } return new UnableToCompleteRequestError(exception); } } ================================================ FILE: src/Ocelot/Requester/IDelegatingHandlerFactory.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Requester; public interface IDelegatingHandlerFactory { List Get(DownstreamRoute route); } ================================================ FILE: src/Ocelot/Requester/IExceptionToErrorMapper.cs ================================================ using Ocelot.Errors; namespace Ocelot.Requester; public interface IExceptionToErrorMapper { Error Map(Exception exception); } ================================================ FILE: src/Ocelot/Requester/IHttpRequester.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Responses; namespace Ocelot.Requester; public interface IHttpRequester { Task> GetResponse(HttpContext httpContext); } ================================================ FILE: src/Ocelot/Requester/IMessageInvokerPool.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Requester; /// /// A pool implementation for pooling. /// /// Largely inspired by StackExchange implementation. /// Link: StackExchange.Utils.DefaultHttpClientPool. /// /// public interface IMessageInvokerPool { /// /// Gets a client for the specified . /// /// The route to get a Message Invoker for. /// A from the pool. HttpMessageInvoker Get(DownstreamRoute downstreamRoute); /// /// Clears the pool, in case you need to. /// void Clear(); } ================================================ FILE: src/Ocelot/Requester/MessageInvokerHttpRequester.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Requester; public class MessageInvokerHttpRequester : IHttpRequester { private readonly IOcelotLogger _logger; private readonly IExceptionToErrorMapper _mapper; private readonly IMessageInvokerPool _messageHandlerPool; public MessageInvokerHttpRequester(IOcelotLoggerFactory loggerFactory, IMessageInvokerPool messageHandlerPool, IExceptionToErrorMapper mapper) { ArgumentNullException.ThrowIfNull(loggerFactory); _logger = loggerFactory.CreateLogger(); ArgumentNullException.ThrowIfNull(messageHandlerPool); _messageHandlerPool = messageHandlerPool; ArgumentNullException.ThrowIfNull(mapper); _mapper = mapper; } public async Task> GetResponse(HttpContext httpContext) { var downstreamRequest = httpContext.Items.DownstreamRequest(); var messageInvoker = _messageHandlerPool.Get(httpContext.Items.DownstreamRoute()); try { var response = await messageInvoker.SendAsync(downstreamRequest.ToHttpRequestMessage(), httpContext.RequestAborted); return new OkResponse(response); } catch (Exception exception) { var error = _mapper.Map(exception); return new ErrorResponse(error); } } } ================================================ FILE: src/Ocelot/Requester/MessageInvokerPool.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Logging; using System.Net.Security; namespace Ocelot.Requester; public class MessageInvokerPool : IMessageInvokerPool { private readonly ConcurrentDictionary> _handlersPool; private readonly IDelegatingHandlerFactory _handlerFactory; private readonly IOcelotLogger _logger; public MessageInvokerPool( IDelegatingHandlerFactory handlerFactory, IOcelotLoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(handlerFactory); ArgumentNullException.ThrowIfNull(loggerFactory); _handlersPool = new(); _handlerFactory = handlerFactory; _logger = loggerFactory.CreateLogger(); } public virtual HttpMessageInvoker Get(DownstreamRoute downstreamRoute) { // Since the comparison is based on the downstream route object reference, // and the QoS Options properties can't be changed after the route is created, // we don't need to use the timeout value as part of the cache key. return _handlersPool.GetOrAdd( new MessageInvokerCacheKey(downstreamRoute), cacheKey => new Lazy(() => CreateMessageInvoker(cacheKey.Route)) ).Value; } public virtual void Clear() => _handlersPool.Clear(); protected HttpMessageInvoker CreateMessageInvoker(DownstreamRoute route) { HttpMessageHandler baseHandler = CreateHandler(route); List handlers = _handlerFactory.Get(route); handlers.Reverse(); foreach (DelegatingHandler handler in handlers) { handler.InnerHandler = baseHandler; baseHandler = handler; } int milliseconds = EnsureRouteTimeoutIsGreaterThanQosOne(route); TimeSpan timeout = TimeSpan.FromMilliseconds(milliseconds); // Adding timeout handler to the top of the chain. // It's standard behavior to throw TimeoutException after the defined timeout (90 seconds by default) HttpMessageHandler timeoutHandler = new TimeoutDelegatingHandler(timeout) { InnerHandler = baseHandler, }; return new(timeoutHandler, true); } /// /// Ensures that the route timeout is greater than the QoS timeout. If the route timeout is less than or equal to the QoS timeout, returns double the QoS timeout value and logs a warning. /// /// The method is open for overriding because it is declared as . /// Current processing route. /// An value representing the timeout in milliseconds, to be assigned in the upper context. protected virtual int EnsureRouteTimeoutIsGreaterThanQosOne(DownstreamRoute route) { var qos = route.QosOptions; int routeMilliseconds = 1_000 * (route.Timeout ?? DownstreamRoute.DefaultTimeoutSeconds); if (!qos.UseQos || !qos.Timeout.HasValue || routeMilliseconds > qos.Timeout) { return routeMilliseconds; } int milliseconds = routeMilliseconds; int doubledTimeout = 2 * qos.Timeout.Value; Func getWarning = route.Timeout.HasValue ? () => $"Route '{route.Name()}' has Quality of Service settings ({nameof(FileRoute.QoSOptions)}) enabled, but either the route {nameof(route.Timeout)} or the QoS {nameof(QoSOptions.Timeout)} is misconfigured: specifically, the route {nameof(route.Timeout)} ({milliseconds} ms) {EqualitySentence(milliseconds, qos.Timeout.Value)} the QoS {nameof(QoSOptions.Timeout)} ({qos.Timeout} ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS {nameof(QoSOptions.Timeout)} and applied {doubledTimeout} ms to the route {nameof(route.Timeout)}. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!" : () => $"Route '{route.Name()}' has Quality of Service settings ({nameof(FileRoute.QoSOptions)}) enabled, but either the {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)} or the QoS {nameof(QoSOptions.Timeout)} is misconfigured: specifically, the {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)} ({milliseconds} ms) {EqualitySentence(milliseconds, qos.Timeout.Value)} the QoS {nameof(QoSOptions.Timeout)} ({qos.Timeout} ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS {nameof(QoSOptions.Timeout)} and applied {doubledTimeout} ms to the route {nameof(route.Timeout)} instead of using {nameof(DownstreamRoute)}.{nameof(DownstreamRoute.DefaultTimeoutSeconds)}. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!"; _logger.LogWarning(getWarning); return doubledTimeout; } public static string EqualitySentence(int left, int right) => left < right ? "is shorter than" : left == right ? "is equal to" : "is longer than"; protected virtual SocketsHttpHandler CreateHandler(DownstreamRoute route) { var options = route.HttpHandlerOptions; var handler = new SocketsHttpHandler { AllowAutoRedirect = options.AllowAutoRedirect, UseCookies = options.UseCookieContainer, UseProxy = options.UseProxy, MaxConnectionsPerServer = options.MaxConnectionsPerServer, PooledConnectionLifetime = options.PooledConnectionLifeTime, }; if (options.UseCookieContainer) { handler.CookieContainer = new CookieContainer(); } if (!route.DangerousAcceptAnyServerCertificateValidator) { return handler; } handler.SslOptions = new SslClientAuthenticationOptions { RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true, }; _logger.LogWarning(() => $"You have ignored all SSL warnings by using {nameof(DownstreamRoute.DangerousAcceptAnyServerCertificateValidator)} for this {nameof(DownstreamRoute)} -> {route.Name()}"); return handler; } public readonly struct MessageInvokerCacheKey : IEquatable { public MessageInvokerCacheKey(DownstreamRoute route) => Route = route; public DownstreamRoute Route { get; } public override bool Equals(object obj) => obj is MessageInvokerCacheKey key && Equals(key); public bool Equals(MessageInvokerCacheKey other) => EqualityComparer.Default.Equals(Route, other.Route); public override int GetHashCode() => Route.GetHashCode(); public static bool operator ==(MessageInvokerCacheKey left, MessageInvokerCacheKey right) => left.Equals(right); public static bool operator !=(MessageInvokerCacheKey left, MessageInvokerCacheKey right) => !(left == right); } } ================================================ FILE: src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Requester.Middleware; public class HttpRequesterMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IHttpRequester _requester; public HttpRequesterMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IHttpRequester requester) : base(loggerFactory.CreateLogger()) { _next = next; _requester = requester; } public async Task Invoke(HttpContext httpContext) { var response = await _requester.GetResponse(httpContext); CreateLogBasedOnResponse(response); if (response.IsError) { Logger.LogDebug("IHttpRequester returned an error, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; } Logger.LogDebug("Setting HTTP response message..."); httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(response.Data)); await _next.Invoke(httpContext); } private void CreateLogBasedOnResponse(Response response) { var status = response.Data?.StatusCode ?? HttpStatusCode.Processing; var reason = response.Data?.ReasonPhrase ?? "unknown"; var uri = response.Data?.RequestMessage?.RequestUri?.ToString() ?? string.Empty; string message() => $"{(int)status} {reason} status code of request URI: {uri}"; if (status < HttpStatusCode.BadRequest) { Logger.LogInformation(message); } else { Logger.LogWarning(message); } } } ================================================ FILE: src/Ocelot/Requester/RequestCanceledError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Requester; public class RequestCanceledError : Error { /// /// Initializes a new instance of the class. /// Creates object by the message. /// Status code refer to: /// https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top . /// https://httpstatuses.com/499 . /// /// The message text. public RequestCanceledError(string message) : base(message, OcelotErrorCode.RequestCanceled, 499) // https://httpstatuses.com/499 { } } ================================================ FILE: src/Ocelot/Requester/ServiceCollectionExtensions.cs ================================================ using Microsoft.Extensions.DependencyInjection; namespace Ocelot.Requester; public static class ServiceCollectionExtensions { public static void AddOcelotMessageInvokerPool(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); } } ================================================ FILE: src/Ocelot/Requester/TimeoutDelegatingHandler.cs ================================================ namespace Ocelot.Requester; /// /// TODO: Next subjects of investigation are: /// /// Request timeouts middleware in ASP.NET Core /// Kestrel timeouts /// /// public class TimeoutDelegatingHandler : DelegatingHandler { private readonly TimeSpan _timeout; /// /// Initializes a new instance of the class. /// /// The time span after which the request is cancelled. public TimeoutDelegatingHandler(TimeSpan timeout) { _timeout = timeout; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); cts.CancelAfter(_timeout); try { return await base.SendAsync(request, cts.Token); } catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) { throw new TimeoutException(); } } } ================================================ FILE: src/Ocelot/Requester/UnableToCompleteRequestError.cs ================================================ using Ocelot.Errors; namespace Ocelot.Requester; public class UnableToCompleteRequestError : Error { public UnableToCompleteRequestError(Exception exception) : base($"Error making http request, exception: {exception}", OcelotErrorCode.UnableToCompleteRequestError, 500) { } } ================================================ FILE: src/Ocelot/Responder/ErrorsToHttpStatusCodeMapper.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; namespace Ocelot.Responder; public class ErrorsToHttpStatusCodeMapper : IErrorsToHttpStatusCodeMapper { public int Map(List errors) { if (errors.Any(e => e.Code == OcelotErrorCode.UnauthenticatedError)) { return 401; } if (errors.Any(e => e.Code == OcelotErrorCode.UnauthorizedError || e.Code == OcelotErrorCode.ClaimValueNotAuthorizedError || e.Code == OcelotErrorCode.ScopeNotAuthorizedError || e.Code == OcelotErrorCode.UserDoesNotHaveClaimError || e.Code == OcelotErrorCode.CannotFindClaimError)) { return 403; } if (errors.Any(e => e.Code == OcelotErrorCode.QuotaExceededError)) { return errors.Single(e => e.Code == OcelotErrorCode.QuotaExceededError).HttpStatusCode; } if (errors.Any(e => e.Code == OcelotErrorCode.RequestTimedOutError)) { return StatusCodes.Status503ServiceUnavailable; } if (errors.Any(e => e.Code == OcelotErrorCode.RequestCanceled)) { // status code refer to // https://stackoverflow.com/questions/46234679/what-is-the-correct-http-status-code-for-a-cancelled-request?answertab=votes#tab-top // https://httpstatuses.com/499 return StatusCodes.Status499ClientClosedRequest; } if (errors.Any(e => e.Code == OcelotErrorCode.UnableToFindDownstreamRouteError)) { return 404; } if (errors.Any(e => e.Code == OcelotErrorCode.ConnectionToDownstreamServiceError)) { return 502; } if (errors.Any(e => e.Code == OcelotErrorCode.UnableToCompleteRequestError || e.Code == OcelotErrorCode.CouldNotFindLoadBalancerCreator || e.Code == OcelotErrorCode.ErrorInvokingLoadBalancerCreator)) { return 500; } if (errors.Any(e => e.Code == OcelotErrorCode.PayloadTooLargeError)) { return 413; } return 404; } } ================================================ FILE: src/Ocelot/Responder/HttpContextResponder.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Primitives; using Ocelot.Headers; using Ocelot.Middleware; namespace Ocelot.Responder; /// /// Cannot unit test things in this class due to methods not being implemented on .NET concretes used for testing. /// public class HttpContextResponder : IHttpResponder { private readonly IRemoveOutputHeaders _removeOutputHeaders; public HttpContextResponder(IRemoveOutputHeaders removeOutputHeaders) { _removeOutputHeaders = removeOutputHeaders; } public async Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse downstream) { _removeOutputHeaders.Remove(downstream.Headers); foreach (var httpResponseHeader in downstream.Headers) { AddHeaderIfDoesntExist(context, httpResponseHeader); } SetStatusCode(context, (int)downstream.StatusCode); context.Response.HttpContext.Features.Get().ReasonPhrase = downstream.ReasonPhrase; // As of 5.0 HttpResponse.Content never returns null. // https://github.com/dotnet/runtime/blame/8fc68f626a11d646109a758cb0fc70a0aa7826f1/src/libraries/System.Net.Http/src/System/Net/Http/HttpResponseMessage.cs#L46 // TODO: Check if it applies to ocelot custom implementation if (downstream.Content is null) { return; } foreach (var httpResponseHeader in downstream.Content.Headers) { AddHeaderIfDoesntExist(context, new Header(httpResponseHeader.Key, httpResponseHeader.Value)); } if (downstream.Content.Headers.ContentLength != null) { AddHeaderIfDoesntExist(context, new Header("Content-Length", new[] { downstream.Content.Headers.ContentLength.ToString() })); } if (downstream.StatusCode != HttpStatusCode.NotModified && context.Response.ContentLength != 0) { await WriteToUpstreamAsync(context, downstream); } } public void SetErrorResponseOnContext(HttpContext context, int statusCode) { SetStatusCode(context, statusCode); } public async Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse downstream) { if (downstream.Content.Headers.ContentLength != null) { AddHeaderIfDoesntExist(context, new Header("Content-Length", new[] { downstream.Content.Headers.ContentLength.ToString() })); } if (context.Response.ContentLength != 0) { await WriteToUpstreamAsync(context, downstream); } } protected virtual async Task WriteToUpstreamAsync(HttpContext context, DownstreamResponse downstream) { await using var content = await downstream.Content.ReadAsStreamAsync(); await content.CopyToAsync(context.Response.Body, context.RequestAborted); } private static void SetStatusCode(HttpContext context, int statusCode) { if (!context.Response.HasStarted) { context.Response.StatusCode = statusCode; } } private static void AddHeaderIfDoesntExist(HttpContext context, Header httpResponseHeader) { if (!context.Response.Headers.ContainsKey(httpResponseHeader.Key)) { context.Response.Headers.Append( httpResponseHeader.Key, new StringValues(httpResponseHeader.Values.ToArray())); } } } ================================================ FILE: src/Ocelot/Responder/IErrorsToHttpStatusCodeMapper.cs ================================================ using Ocelot.Errors; namespace Ocelot.Responder; /// /// Defines mapping a list of Ocelot errors to a single appropriate HTTP status code. /// public interface IErrorsToHttpStatusCodeMapper { /// /// Maps a list of Ocelot to a single appropriate HTTP status code. /// /// The collection of errors. /// An integer value with HTTP status code. int Map(List errors); } ================================================ FILE: src/Ocelot/Responder/IHttpResponder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Middleware; namespace Ocelot.Responder; public interface IHttpResponder { Task SetResponseOnHttpContext(HttpContext context, DownstreamResponse response); void SetErrorResponseOnContext(HttpContext context, int statusCode); Task SetErrorResponseOnContext(HttpContext context, DownstreamResponse response); } ================================================ FILE: src/Ocelot/Responder/Middleware/ResponderMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Responder.Middleware; /// /// Completes and returns the request and request body, if any pipeline errors occured then sets the appropriate HTTP status code instead. /// public class ResponderMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IHttpResponder _responder; private readonly IErrorsToHttpStatusCodeMapper _codeMapper; public ResponderMiddleware(RequestDelegate next, IHttpResponder responder, IOcelotLoggerFactory loggerFactory, IErrorsToHttpStatusCodeMapper codeMapper) : base(loggerFactory.CreateLogger()) { _next = next; _responder = responder; _codeMapper = codeMapper; } public async Task Invoke(HttpContext context) { await _next.Invoke(context); var errors = context.Items.Errors(); if (errors.Count > 0) { Logger.LogWarning(() => $"{MiddlewareName} found {errors.Count} error{errors.Count.Plural()} ->{errors.ToErrorString(true, true)}Setting error response for request: {context.Request.Method} {context.Request.Path}"); await SetErrorResponse(context, errors); return; } // We are going to dispose the http request message and content in // this middleware (no further use). That's why we are using the 'using' statement. using var response = context.Items.DownstreamResponse(); if (response == null) { Logger.LogDebug(() => $"Pipeline was terminated early in {MiddlewareName}"); return; } Logger.LogDebug("No pipeline errors: setting and returning completed response..."); await _responder.SetResponseOnHttpContext(context, response); } private async Task SetErrorResponse(HttpContext context, List errors) { // TODO The exception/error handling should be reviewed and refactored. var statusCode = _codeMapper.Map(errors); _responder.SetErrorResponseOnContext(context, statusCode); if (errors.All(e => e.Code != OcelotErrorCode.QuotaExceededError)) { return; } var downstreamResponse = context.Items.DownstreamResponse(); await _responder.SetErrorResponseOnContext(context, downstreamResponse); } } ================================================ FILE: src/Ocelot/Responses/ErrorResponse.cs ================================================ using Ocelot.Errors; namespace Ocelot.Responses; public class ErrorResponse : Response { public ErrorResponse(Error error) : base(new() { error }) { } public ErrorResponse(List errors) : base(errors) { } } public class ErrorResponse : Response { public ErrorResponse(Error error) : base(new List { error }) { } public ErrorResponse(List errors) : base(errors) { } } ================================================ FILE: src/Ocelot/Responses/OkResponse.cs ================================================ namespace Ocelot.Responses; public class OkResponse : Response { public OkResponse() { } } public class OkResponse : Response { public OkResponse(T data) : base(data) { } } ================================================ FILE: src/Ocelot/Responses/Response.cs ================================================ using Ocelot.Errors; namespace Ocelot.Responses; public abstract class Response { protected Response() { Errors = new List(); } protected Response(List errors) { Errors = errors ?? new List(); } public List Errors { get; } public bool IsError => Errors.Count > 0; } public abstract class Response : Response { protected Response(T data) { Data = data; } protected Response(List errors) : base(errors) { } public T Data { get; } } ================================================ FILE: src/Ocelot/Security/IPSecurity/IPSecurityPolicy.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Security.IPSecurity; public class IPSecurityPolicy : ISecurityPolicy { public Response Security(DownstreamRoute downstreamRoute, HttpContext context) { var clientIp = context.Connection.RemoteIpAddress; var options = downstreamRoute.SecurityOptions; if (options == null || clientIp == null) { return new OkResponse(); } if (options.IPBlockedList?.Count > 0) { if (options.IPBlockedList.Contains(clientIp.ToString())) { var error = new UnauthenticatedError($"This request rejects access to {clientIp} IP"); return new ErrorResponse(error); } } if (options.IPAllowedList?.Count > 0) { if (!options.IPAllowedList.Contains(clientIp.ToString())) { var error = new UnauthenticatedError($"{clientIp} does not allow access, the request is invalid"); return new ErrorResponse(error); } } return new OkResponse(); } public Task SecurityAsync(DownstreamRoute downstreamRoute, HttpContext context) => Task.Run(() => Security(downstreamRoute, context)); } ================================================ FILE: src/Ocelot/Security/ISecurityPolicy.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Responses; namespace Ocelot.Security; public interface ISecurityPolicy { Response Security(DownstreamRoute downstreamRoute, HttpContext context); Task SecurityAsync(DownstreamRoute downstreamRoute, HttpContext context); } ================================================ FILE: src/Ocelot/Security/Middleware/SecurityMiddleware.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Security.Middleware; public class SecurityMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IEnumerable _securityPolicies; public SecurityMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IEnumerable securityPolicies) : base(loggerFactory.CreateLogger()) { _securityPolicies = securityPolicies; _next = next; } public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); if (_securityPolicies != null) { foreach (var policy in _securityPolicies) { var result = policy.Security(downstreamRoute, httpContext); if (!result.IsError) { continue; } httpContext.Items.UpsertErrors(result.Errors); return; } } await _next.Invoke(httpContext); } } ================================================ FILE: src/Ocelot/ServiceDiscovery/Configuration/ServiceFabricConfiguration.cs ================================================ namespace Ocelot.ServiceDiscovery.Configuration; public class ServiceFabricConfiguration { public ServiceFabricConfiguration(string hostName, int port, string serviceName) { HostName = hostName; Port = port; ServiceName = serviceName; } public string ServiceName { get; } public string HostName { get; } public int Port { get; } } ================================================ FILE: src/Ocelot/ServiceDiscovery/IServiceDiscoveryProviderFactory.cs ================================================ using Ocelot.Configuration; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.ServiceDiscovery; public interface IServiceDiscoveryProviderFactory { Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route); } ================================================ FILE: src/Ocelot/ServiceDiscovery/Providers/ConfigurationServiceProvider.cs ================================================ using Ocelot.Values; namespace Ocelot.ServiceDiscovery.Providers; public class ConfigurationServiceProvider : IServiceDiscoveryProvider { private readonly List _services; public ConfigurationServiceProvider(List services) => _services = services; public Task> GetAsync() => ValueTask.FromResult(_services).AsTask(); } ================================================ FILE: src/Ocelot/ServiceDiscovery/Providers/IServiceDiscoveryProvider.cs ================================================ using Ocelot.Values; namespace Ocelot.ServiceDiscovery.Providers; public interface IServiceDiscoveryProvider { Task> GetAsync(); } ================================================ FILE: src/Ocelot/ServiceDiscovery/Providers/ServiceFabricServiceDiscoveryProvider.cs ================================================ using Ocelot.ServiceDiscovery.Configuration; using Ocelot.Values; namespace Ocelot.ServiceDiscovery.Providers; public class ServiceFabricServiceDiscoveryProvider : IServiceDiscoveryProvider { public const string Type = "ServiceFabric"; // TODO This property should be defined in the IServiceDiscoveryProvider interface private readonly ServiceFabricConfiguration _configuration; public ServiceFabricServiceDiscoveryProvider(ServiceFabricConfiguration configuration) { _configuration = configuration; } public Task> GetAsync() { return Task.FromResult(new List { new(_configuration.ServiceName, new ServiceHostAndPort(_configuration.HostName, _configuration.Port), "doesnt matter with service fabric", "doesnt matter with service fabric", new List()), }); } } ================================================ FILE: src/Ocelot/ServiceDiscovery/ServiceDiscoveryFinderDelegate.cs ================================================ using Ocelot.Configuration; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.ServiceDiscovery; public delegate IServiceDiscoveryProvider ServiceDiscoveryFinderDelegate(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route); ================================================ FILE: src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Configuration; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.ServiceDiscovery; public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { private readonly IServiceProvider _provider; private readonly ServiceDiscoveryFinderDelegate _delegates; private readonly IOcelotLogger _logger; public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) { _provider = provider; _delegates = provider.GetService(); _logger = factory.CreateLogger(); } public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { if (route.UseServiceDiscovery) { _logger.LogInformation(() => $"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{route.Name()}' is enabled."); return GetServiceDiscoveryProvider(serviceConfig, route); } var services = route.DownstreamAddresses .Select(address => new Service( route.ServiceName, new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme), string.Empty, string.Empty, Enumerable.Empty())) .ToList(); return new OkResponse(new ConfigurationServiceProvider(services)); } private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route) { _logger.LogInformation(() => $"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); if (ServiceFabricServiceDiscoveryProvider.Type.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); return new OkResponse(new ServiceFabricServiceDiscoveryProvider(sfConfig)); } if (_delegates != null) { var provider = _delegates?.Invoke(_provider, config, route); if (provider.GetType().Name.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { return new OkResponse(provider); } } var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; _logger.LogWarning(() => $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"); return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError(message)); } } ================================================ FILE: src/Ocelot/ServiceDiscovery/UnableToFindServiceDiscoveryProviderError.cs ================================================ using Ocelot.Errors; namespace Ocelot.ServiceDiscovery; public class UnableToFindServiceDiscoveryProviderError : Error { public UnableToFindServiceDiscoveryProviderError(string message) : base(message, OcelotErrorCode.UnableToFindServiceDiscoveryProviderError, 404) { } } ================================================ FILE: src/Ocelot/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using System.Collections.Concurrent; global using System.Net; global using System.Text; global using System.Text.RegularExpressions; ================================================ FILE: src/Ocelot/Values/DownstreamPath.cs ================================================ namespace Ocelot.Values; public class DownstreamPath { public DownstreamPath(string value) { Value = value; } public string Value { get; } } ================================================ FILE: src/Ocelot/Values/DownstreamPathTemplate.cs ================================================ namespace Ocelot.Values; public class DownstreamPathTemplate { public DownstreamPathTemplate(string value) { Value = value; } public string Value { get; } public override string ToString() => Value ?? string.Empty; } ================================================ FILE: src/Ocelot/Values/Service.cs ================================================ namespace Ocelot.Values; public class Service { public Service(string name, ServiceHostAndPort hostAndPort, string id, string version, IEnumerable tags) { Name = name; HostAndPort = hostAndPort; Id = id; Version = version; Tags = tags; } public string Id { get; } public string Name { get; } public string Version { get; } public IEnumerable Tags { get; } public ServiceHostAndPort HostAndPort { get; } } ================================================ FILE: src/Ocelot/Values/ServiceHostAndPort.cs ================================================ namespace Ocelot.Values; public class ServiceHostAndPort : IEquatable { public ServiceHostAndPort(ServiceHostAndPort from) { DownstreamHost = from.DownstreamHost; DownstreamPort = from.DownstreamPort; Scheme = from.Scheme; } public ServiceHostAndPort(string downstreamHost, int downstreamPort) { DownstreamHost = downstreamHost?.Trim('/'); DownstreamPort = downstreamPort; } public ServiceHostAndPort(string downstreamHost, int downstreamPort, string scheme) : this(downstreamHost, downstreamPort) => Scheme = scheme; public string DownstreamHost { get; } public int DownstreamPort { get; } public string Scheme { get; } public override string ToString() => $"{Scheme}:{DownstreamHost}:{DownstreamPort}"; public override int GetHashCode() => Tuple.Create(Scheme, DownstreamHost, DownstreamPort).GetHashCode(); public bool Equals(ServiceHostAndPort other) => this == other; public override bool Equals(object obj) => obj != null && obj is ServiceHostAndPort o && this == o; /// Checks equality of two hosts. /// Microsoft Learn | .NET | C# Docs: /// /// Equality operators /// System.Object.Equals method /// IEquatable<T>.Equals(T) Method /// /// /// Left operand. /// Right operand. /// if both operands are equal; otherwise, . public static bool operator ==(ServiceHostAndPort l, ServiceHostAndPort r) => (((object)l) == null || ((object)r) == null) ? Equals(l, r) : l.DownstreamHost == r.DownstreamHost && l.DownstreamPort == r.DownstreamPort && l.Scheme == r.Scheme; public static bool operator !=(ServiceHostAndPort l, ServiceHostAndPort r) => (((object)l) == null || ((object)r) == null) ? !Equals(l, r) : !(l == r); } ================================================ FILE: src/Ocelot/Values/UpstreamHeaderTemplate.cs ================================================ using Ocelot.Infrastructure; namespace Ocelot.Values; /// /// Upstream template properties of headers and their regular expression. /// /// Ocelot feature: Routing based on request header. public class UpstreamHeaderTemplate { public string Template { get; } public string OriginalValue { get; } public Regex Pattern { get; } public UpstreamHeaderTemplate(string template, string originalValue) { Template = template; OriginalValue = originalValue; Pattern = RegexGlobal.New(template ?? "$^", RegexOptions.Singleline); } } ================================================ FILE: src/Ocelot/Values/UpstreamPathTemplate.cs ================================================ using Ocelot.Infrastructure; namespace Ocelot.Values; /// The model to keep data of upstream path. public partial class UpstreamPathTemplate { [GeneratedRegex("$^", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex RegexNoTemplate(); public UpstreamPathTemplate(string template, int priority, bool containsQueryString, string originalValue) { Template = template; Priority = priority; ContainsQueryString = containsQueryString; OriginalValue = originalValue; } public string Template { get; } public int Priority { get; } public bool ContainsQueryString { get; } public string OriginalValue { get; } private Regex _pattern; public Regex Pattern { get => _pattern; set => _pattern = Template == null || value == null ? RegexNoTemplate() : value; } } ================================================ FILE: src/Ocelot/WebSockets/ClientWebSocketConnector.cs ================================================ using System.Net.WebSockets; namespace Ocelot.WebSockets; public class ClientWebSocketConnector : IClientWebSocketConnector { private readonly ClientWebSocket _webSocket; private readonly IClientWebSocketOptions _options; public ClientWebSocketConnector(ClientWebSocket webSocket) { _webSocket = webSocket; _options = new ClientWebSocketOptionsProxy(webSocket.Options); } public WebSocket ToWebSocket() => _webSocket; public IClientWebSocketOptions Options => _options; public Task ConnectAsync(Uri uri, CancellationToken cancellationToken) => _webSocket.ConnectAsync(uri, cancellationToken); } ================================================ FILE: src/Ocelot/WebSockets/ClientWebSocketOptionsProxy.cs ================================================ using System.Net.Security; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; namespace Ocelot.WebSockets; public class ClientWebSocketOptionsProxy : IClientWebSocketOptions { private readonly ClientWebSocketOptions _real; public ClientWebSocketOptionsProxy(ClientWebSocketOptions options) { _real = options; } // TODO The design should be reviewed since we are hiding the ClientWebSocketOptions properties. public Version HttpVersion { get => _real.HttpVersion; set => _real.HttpVersion = value; } public HttpVersionPolicy HttpVersionPolicy { get => _real.HttpVersionPolicy; set => _real.HttpVersionPolicy = value; } public bool UseDefaultCredentials { get => _real.UseDefaultCredentials; set => _real.UseDefaultCredentials = value; } public ICredentials Credentials { get => _real.Credentials; set => _real.Credentials = value; } public IWebProxy Proxy { get => _real.Proxy; set => _real.Proxy = value; } public X509CertificateCollection ClientCertificates { get => _real.ClientCertificates; set => _real.ClientCertificates = value; } public RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get => _real.RemoteCertificateValidationCallback; set => _real.RemoteCertificateValidationCallback = value; } public CookieContainer Cookies { get => _real.Cookies; set => _real.Cookies = value; } public TimeSpan KeepAliveInterval { get => _real.KeepAliveInterval; set => _real.KeepAliveInterval = value; } public WebSocketDeflateOptions DangerousDeflateOptions { get => _real.DangerousDeflateOptions; set => _real.DangerousDeflateOptions = value; } public bool CollectHttpResponseDetails { get => _real.CollectHttpResponseDetails; set => _real.CollectHttpResponseDetails = value; } public void AddSubProtocol(string subProtocol) => _real.AddSubProtocol(subProtocol); public void SetBuffer(int receiveBufferSize, int sendBufferSize) => _real.SetBuffer(receiveBufferSize, sendBufferSize); public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment buffer) => _real.SetBuffer(receiveBufferSize, sendBufferSize, buffer); public void SetRequestHeader(string headerName, string headerValue) => _real.SetRequestHeader(headerName, headerValue); } ================================================ FILE: src/Ocelot/WebSockets/ClientWebSocketProxy.cs ================================================ using System.Net.WebSockets; namespace Ocelot.WebSockets; public sealed class ClientWebSocketProxy : WebSocket, IClientWebSocket { // RealSubject (Service) class of Proxy design pattern private readonly WebSocket _realSocket; private readonly IClientWebSocketConnector _connector; public ClientWebSocketProxy(WebSocket socket, IClientWebSocketConnector connector) { _realSocket = socket; _connector = connector; } // IClientWebSocketConnector implementations public WebSocket ToWebSocket() => _realSocket; public IClientWebSocketOptions Options => _connector.Options; public Task ConnectAsync(Uri uri, CancellationToken cancellationToken) => _connector.ConnectAsync(uri, cancellationToken); // WebSocket implementations public override WebSocketCloseStatus? CloseStatus => _realSocket.CloseStatus; public override string CloseStatusDescription => _realSocket.CloseStatusDescription; public override WebSocketState State => _realSocket.State; public override string SubProtocol => _realSocket.SubProtocol; public override void Abort() => _realSocket.Abort(); public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => _realSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => _realSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken); public override void Dispose() => _realSocket.Dispose(); public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) => _realSocket.ReceiveAsync(buffer, cancellationToken); public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => _realSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); } ================================================ FILE: src/Ocelot/WebSockets/IClientWebSocket.cs ================================================ using System.Net.WebSockets; namespace Ocelot.WebSockets; public interface IClientWebSocket : IClientWebSocketConnector { // WebSocket definitions WebSocketCloseStatus? CloseStatus { get; } string CloseStatusDescription { get; } WebSocketState State { get; } string SubProtocol { get; } void Abort(); Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken); Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken); void Dispose(); Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken); Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); } public interface IClientWebSocketConnector { WebSocket ToWebSocket(); IClientWebSocketOptions Options { get; } Task ConnectAsync(Uri uri, CancellationToken cancellationToken); } ================================================ FILE: src/Ocelot/WebSockets/IClientWebSocketOptions.cs ================================================ using System.Net.Security; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; namespace Ocelot.WebSockets; public interface IClientWebSocketOptions { Version HttpVersion { get; set; } HttpVersionPolicy HttpVersionPolicy { get; set; } void SetRequestHeader(string headerName, string headerValue); bool UseDefaultCredentials { get; set; } ICredentials Credentials { get; set; } IWebProxy Proxy { get; set; } X509CertificateCollection ClientCertificates { get; set; } RemoteCertificateValidationCallback RemoteCertificateValidationCallback { get; set; } CookieContainer Cookies { get; set; } void AddSubProtocol(string subProtocol); TimeSpan KeepAliveInterval { get; set; } WebSocketDeflateOptions DangerousDeflateOptions { get; set; } void SetBuffer(int receiveBufferSize, int sendBufferSize); void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment buffer); bool CollectHttpResponseDetails { get; set; } } ================================================ FILE: src/Ocelot/WebSockets/IWebSocketsFactory.cs ================================================ namespace Ocelot.WebSockets; public interface IWebSocketsFactory { IClientWebSocket CreateClient(); } ================================================ FILE: src/Ocelot/WebSockets/WebSocketsFactory.cs ================================================ using System.Net.WebSockets; namespace Ocelot.WebSockets; public class WebSocketsFactory : IWebSocketsFactory { public IClientWebSocket CreateClient() { var socket = new ClientWebSocket(); var connector = new ClientWebSocketConnector(socket); return new ClientWebSocketProxy(socket, connector); } } ================================================ FILE: src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs ================================================ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Modified https://github.com/aspnet/Proxy websockets class to use in Ocelot. using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using System.Net.WebSockets; namespace Ocelot.WebSockets; public class WebSocketsProxyMiddleware : OcelotMiddleware { public static readonly string[] NotForwardedWebSocketHeaders = new[] { "Connection", "Host", "Upgrade", "Sec-WebSocket-Accept", "Sec-WebSocket-Protocol", "Sec-WebSocket-Key", "Sec-WebSocket-Version", "Sec-WebSocket-Extensions", }; private const int DefaultWebSocketBufferSize = 4096; private readonly RequestDelegate _next; private readonly IWebSocketsFactory _factory; public const string IgnoredSslWarningFormat = $"You have ignored all SSL warnings by using {nameof(DownstreamRoute.DangerousAcceptAnyServerCertificateValidator)} for this downstream route! {nameof(DownstreamRoute.UpstreamPathTemplate)}: '{{0}}', {nameof(DownstreamRoute.DownstreamPathTemplate)}: '{{1}}'."; public const string InvalidSchemeWarningFormat = "Invalid scheme has detected which will be replaced! Scheme '{0}' of the downstream '{1}'."; public WebSocketsProxyMiddleware(IOcelotLoggerFactory logging, RequestDelegate next, IWebSocketsFactory factory) : base(logging.CreateLogger()) { _next = next; _factory = factory; } public async Task Invoke(HttpContext context) { var request = context.Items.DownstreamRequest(); var route = context.Items.DownstreamRoute(); await Proxy(context, request, route); } protected virtual async Task PumpAsync(WebSocket source, WebSocket destination, int bufferSize, CancellationToken cancellation) { ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize); var buffer = new byte[bufferSize]; while (true) { WebSocketReceiveResult result = default; try { result = await source.ReceiveAsync(new ArraySegment(buffer), cancellation); } catch (OperationCanceledException) { await TryCloseOutputAsync(destination, WebSocketCloseStatus.EndpointUnavailable, nameof(OperationCanceledException), cancellation); return; // we don't rethrow timeout/cancellation errors } catch (WebSocketException e) { if (e.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { await TryCloseOutputAsync(destination, WebSocketCloseStatus.EndpointUnavailable, $"{nameof(WebSocketException)} when {nameof(e.WebSocketErrorCode)} is {nameof(WebSocketError.ConnectionClosedPrematurely)}", cancellation); } // DON'T THROW, NEVER! Just log the warning... // The logging level has been decreased from level 4 (Error) to level 3 (Warning) due to the high number of disconnecting events for sensitive WebSocket connections in unstable networks. Logger.LogWarning(() => $"{nameof(WebSocketException)} when {nameof(e.WebSocketErrorCode)} is {e.WebSocketErrorCode}"); return; // swallow the error } if (result.MessageType == WebSocketMessageType.Close) { await TryCloseOutputAsync(destination, source.CloseStatus.Value, source.CloseStatusDescription, cancellation); return; } if (destination.State == WebSocketState.Open) { await destination.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, cancellation); } } } private async Task Proxy(HttpContext context, DownstreamRequest request, DownstreamRoute route) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); ArgumentNullException.ThrowIfNull(route); if (!context.WebSockets.IsWebSocketRequest) { throw new InvalidOperationException(); } var client = _factory.CreateClient(); // new ClientWebSocket(); if (route.DangerousAcceptAnyServerCertificateValidator) { client.Options.RemoteCertificateValidationCallback = (request, certificate, chain, errors) => true; Logger.LogWarning(() => string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate)); } foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) { client.Options.AddSubProtocol(protocol); } foreach (var header in context.Request.Headers) { if (!NotForwardedWebSocketHeaders.Contains(header.Key, StringComparer.OrdinalIgnoreCase)) { try { client.Options.SetRequestHeader(header.Key, header.Value); } catch (ArgumentException) { // Expected in .NET Framework for headers that are mistakenly considered restricted. // See: https://github.com/dotnet/corefx/issues/26627 // .NET Core does not exhibit this issue, ironically due to a separate bug (https://github.com/dotnet/corefx/issues/18784) } } } // Only Uris starting with 'ws://' or 'wss://' are supported in System.Net.WebSockets.ClientWebSocket var scheme = request.Scheme; if (!scheme.StartsWith(Uri.UriSchemeWs)) { Logger.LogWarning(() => string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri())); request.Scheme = scheme == Uri.UriSchemeHttp ? Uri.UriSchemeWs : scheme == Uri.UriSchemeHttps ? Uri.UriSchemeWss : scheme; } var destinationUri = new Uri(request.ToUri()); await client.ConnectAsync(destinationUri, context.RequestAborted); using var server = await context.WebSockets.AcceptWebSocketAsync(client.SubProtocol); await Task.WhenAll( PumpAsync(client.ToWebSocket(), server, DefaultWebSocketBufferSize, context.RequestAborted), PumpAsync(server, client.ToWebSocket(), DefaultWebSocketBufferSize, context.RequestAborted)); } /// /// Closes the WebSocket only if its state is or . /// /// The underlying closing task if the matches; otherwise, the . protected virtual Task TryCloseOutputAsync(WebSocket webSocket, WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellation) => (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived) ? webSocket.CloseOutputAsync(closeStatus, statusDescription, cancellation) : Task.CompletedTask; } ================================================ FILE: src/Ocelot/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "FluentValidation": { "type": "Direct", "requested": "[12.1.1, )", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Direct", "requested": "[6.3.0, )", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Direct", "requested": "[3.1.32, )", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } } }, "net10.0/osx-x64": {}, "net10.0/win-x64": {}, "net8.0": { "FluentValidation": { "type": "Direct", "requested": "[12.1.1, )", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Direct", "requested": "[6.3.0, )", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Direct", "requested": "[8.0.25, )", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", "requested": "[8.0.25, )", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Direct", "requested": "[3.1.32, )", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } } }, "net8.0/osx-x64": {}, "net8.0/win-x64": {}, "net9.0": { "FluentValidation": { "type": "Direct", "requested": "[12.1.1, )", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Direct", "requested": "[6.3.0, )", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Direct", "requested": "[9.0.14, )", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", "requested": "[9.0.14, )", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Direct", "requested": "[3.1.32, )", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } } }, "net9.0/osx-x64": {}, "net9.0/win-x64": {} } } ================================================ FILE: src/Ocelot.Provider.Consul/Consul.cs ================================================ using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.Provider.Consul; public class Consul : IServiceDiscoveryProvider { private readonly ConsulRegistryConfiguration _configuration; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; private readonly IConsulServiceBuilder _serviceBuilder; public Consul( ConsulRegistryConfiguration config, IOcelotLoggerFactory factory, IConsulClientFactory clientFactory, IConsulServiceBuilder serviceBuilder) { _configuration = config; _consul = clientFactory.Get(_configuration); _logger = factory.CreateLogger(); _serviceBuilder = serviceBuilder; } public virtual async Task> GetAsync() { var entriesTask = _consul.Health.Service(_configuration.KeyOfServiceInConsul, string.Empty, true); var nodesTask = _consul.Catalog.Nodes(); await Task.WhenAll(entriesTask, nodesTask); var entries = (await entriesTask).Response ?? Array.Empty(); var nodes = (await nodesTask).Response ?? Array.Empty(); if (entries.Length == 0) { _logger.LogWarning(() => $"{nameof(Consul)} Provider: No service entries found for '{_configuration.KeyOfServiceInConsul}' service!"); return new(); } _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {entries.Length} service entries for '{_configuration.KeyOfServiceInConsul}' service."); _logger.LogDebug(() => $"{nameof(Consul)} Provider: Found total {nodes.Length} catalog nodes."); return BuildServices(entries, nodes) .ToList(); } protected virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) => _serviceBuilder.BuildServices(entries, nodes); } ================================================ FILE: src/Ocelot.Provider.Consul/ConsulClientFactory.cs ================================================ using Ocelot.Provider.Consul.Interfaces; namespace Ocelot.Provider.Consul; public class ConsulClientFactory : IConsulClientFactory { // TODO We need this overloaded method -> //public IConsulClient Get(ServiceProviderConfiguration config) public IConsulClient Get(ConsulRegistryConfiguration config) => new ConsulClient(c => OverrideConfig(c, config)); // TODO -> //private static void OverrideConfig(ConsulClientConfiguration to, ServiceProviderConfiguration from) // Factory which consumes concrete types is a bad factory! A more abstract types are required private static void OverrideConfig(ConsulClientConfiguration to, ConsulRegistryConfiguration from) // TODO Why ConsulRegistryConfiguration? We use ServiceProviderConfiguration props only! :) { to.Address = new Uri($"{from.Scheme}://{from.Host}:{from.Port}"); if (!string.IsNullOrEmpty(from?.Token)) { to.Token = from.Token; } } } ================================================ FILE: src/Ocelot.Provider.Consul/ConsulFileConfigurationRepository.cs ================================================ using Microsoft.Extensions.Options; using Newtonsoft.Json; using Ocelot.Cache; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; namespace Ocelot.Provider.Consul; public class ConsulFileConfigurationRepository : IFileConfigurationRepository { private readonly IOcelotCache _cache; private readonly string _configurationKey; private readonly IConsulClient _consul; private readonly IOcelotLogger _logger; public ConsulFileConfigurationRepository( IOptions fileConfiguration, IOcelotCache cache, IConsulClientFactory factory, IOcelotLoggerFactory loggerFactory) { _logger = loggerFactory.CreateLogger(); _cache = cache; var provider = fileConfiguration.Value.GlobalConfiguration.ServiceDiscoveryProvider; _configurationKey = string.IsNullOrWhiteSpace(provider.ConfigurationKey) ? nameof(InternalConfiguration) : provider.ConfigurationKey; var config = new ConsulRegistryConfiguration(provider.Scheme, provider.Host, provider.Port, _configurationKey, provider.Token); _consul = factory.Get(config); } public async Task> Get() { var config = _cache.Get(_configurationKey, _configurationKey); if (config != null) { return new OkResponse(config); } var queryResult = await _consul.KV.Get(_configurationKey); if (queryResult.Response == null) { return new OkResponse(null); } var bytes = queryResult.Response.Value; var json = Encoding.UTF8.GetString(bytes); var consulConfig = JsonConvert.DeserializeObject(json); return new OkResponse(consulConfig); } public async Task Set(FileConfiguration ocelotConfiguration) { var json = JsonConvert.SerializeObject(ocelotConfiguration, Formatting.Indented); var bytes = Encoding.UTF8.GetBytes(json); var kvPair = new KVPair(_configurationKey) { Value = bytes, }; var result = await _consul.KV.Put(kvPair); if (result.Response) { _cache.AddOrUpdate(_configurationKey, ocelotConfiguration, _configurationKey, TimeSpan.FromSeconds(3)); return new OkResponse(); } return new ErrorResponse(new UnableToSetConfigInConsulError( $"Unable to set {nameof(FileConfiguration)} in {nameof(Consul)}, response status code from {nameof(Consul)} was {result.StatusCode}")); } } ================================================ FILE: src/Ocelot.Provider.Consul/ConsulMiddlewareConfigurationProvider.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Infrastructure.Extensions; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.Provider.Consul; public static class ConsulMiddlewareConfigurationProvider { public static OcelotMiddlewareConfigurationDelegate Get { get; } = GetAsync; private static async Task GetAsync(IApplicationBuilder builder) { var fileConfigRepo = builder.ApplicationServices.GetService(); var fileConfig = builder.ApplicationServices.GetService>(); var internalConfigCreator = builder.ApplicationServices.GetService(); var internalConfigRepo = builder.ApplicationServices.GetService(); if (UsingConsul(fileConfigRepo)) { await SetFileConfigInConsul(builder, fileConfigRepo, fileConfig, internalConfigCreator, internalConfigRepo); } } private static bool UsingConsul(IFileConfigurationRepository fileConfigRepo) => fileConfigRepo.GetType() == typeof(ConsulFileConfigurationRepository); private static async Task SetFileConfigInConsul(IApplicationBuilder builder, IFileConfigurationRepository fileConfigRepo, IOptionsMonitor fileConfig, IInternalConfigurationCreator internalConfigCreator, IInternalConfigurationRepository internalConfigRepo) { // Get the config from Consul var fileConfigFromConsul = await fileConfigRepo.Get(); if (IsError(fileConfigFromConsul)) { ThrowToStopOcelotStarting(fileConfigFromConsul); } else if (ConfigNotStoredInConsul(fileConfigFromConsul)) { // there was no config in Consul set the file in config in Consul await fileConfigRepo.Set(fileConfig.CurrentValue); } else { // Create the internal config from Consul data var internalConfig = await internalConfigCreator.Create(fileConfigFromConsul.Data); if (IsError(internalConfig)) { ThrowToStopOcelotStarting(internalConfig); } else { // add the internal config to the internal repo var response = internalConfigRepo.AddOrReplace(internalConfig.Data); if (IsError(response)) { ThrowToStopOcelotStarting(response); } } if (IsError(internalConfig)) { ThrowToStopOcelotStarting(internalConfig); } } } private static void ThrowToStopOcelotStarting(Response config) => throw new Exception($"Unable to start Ocelot, errors are:{config.Errors.ToErrorString(true, true)}"); private static bool IsError(Response response) => response == null || response.IsError; private static bool ConfigNotStoredInConsul(Response fileConfigFromConsul) => fileConfigFromConsul.Data == null; } ================================================ FILE: src/Ocelot.Provider.Consul/ConsulProviderFactory.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Provider.Consul; /// /// TODO It must be refactored converting to real factory-class and add to DI. /// /// /// Must inherit from interface. /// Also the must be removed from the design. /// public static class ConsulProviderFactory // TODO : IServiceDiscoveryProviderFactory { /// String constant used for provider type definition. public const string PollConsul = nameof(Provider.Consul.PollConsul); private static readonly List ServiceDiscoveryProviders = new(); // TODO It must be singleton service in DI-container #if NET9_0_OR_GREATER private static readonly System.Threading.Lock SyncRoot = new(); #else private static readonly object SyncRoot = new(); #endif public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { // Singleton services var factory = provider.GetService(); var consulFactory = provider.GetService(); var contextAccessor = provider.GetService(); // Scoped services var context = contextAccessor.HttpContext; var configuration = new ConsulRegistryConfiguration(config.Scheme, config.Host, config.Port, route.ServiceName, config.Token); // TODO Why not to pass 2 args only: config, route? LoL context.Items[nameof(ConsulRegistryConfiguration)] = configuration; // initialize data var serviceBuilder = context.RequestServices.GetService(); // consume data in default/custom builder var consulProvider = new Consul(configuration, factory, consulFactory, serviceBuilder); // TODO It must be added to DI-container! if (PollConsul.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { lock (SyncRoot) { var discoveryProvider = ServiceDiscoveryProviders.FirstOrDefault(x => x.ServiceName == route.ServiceName); if (discoveryProvider != null) { return discoveryProvider; } discoveryProvider = new PollConsul(config.PollingInterval, route.ServiceName, factory, consulProvider); ServiceDiscoveryProviders.Add(discoveryProvider); return discoveryProvider; } } return consulProvider; } } ================================================ FILE: src/Ocelot.Provider.Consul/ConsulRegistryConfiguration.cs ================================================ namespace Ocelot.Provider.Consul; public class ConsulRegistryConfiguration // TODO Inherit from ServiceProviderConfiguration ? { /// /// Consul HTTP client default port. /// /// HashiCorp Developer docs: Consul | Required Ports. /// /// public const int DefaultHttpPort = 8500; public ConsulRegistryConfiguration(string scheme, string host, int port, string keyOfServiceInConsul, string token) { // TODO Why not to encapsulate this biz logic right in ConsulProviderFactory? LoL Host = string.IsNullOrEmpty(host) ? "localhost" : host; Port = port > 0 ? port : DefaultHttpPort; Scheme = string.IsNullOrEmpty(scheme) ? Uri.UriSchemeHttp : scheme; KeyOfServiceInConsul = keyOfServiceInConsul; Token = token; } public string KeyOfServiceInConsul { get; } public string Scheme { get; } public string Host { get; } public int Port { get; } public string Token { get; } } ================================================ FILE: src/Ocelot.Provider.Consul/DefaultConsulServiceBuilder.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.Extensions; using Ocelot.Logging; using Ocelot.Provider.Consul.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Consul; public class DefaultConsulServiceBuilder : IConsulServiceBuilder { private readonly HttpContext _context; private readonly IConsulClientFactory _clientFactory; private readonly IOcelotLoggerFactory _loggerFactory; private ConsulRegistryConfiguration _configuration; private IConsulClient _client; private IOcelotLogger _logger; public DefaultConsulServiceBuilder( IHttpContextAccessor contextAccessor, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) { _context = contextAccessor.HttpContext; _clientFactory = clientFactory; _loggerFactory = loggerFactory; } // TODO See comment in the interface about the privacy. The goal is to eliminate IBC! // So, we need more abstract type, and ServiceProviderConfiguration is a good choice. The rest of props can be obtained from HttpContext protected /*public*/ ConsulRegistryConfiguration Configuration => _configuration ??= _context.Items.TryGetValue(nameof(ConsulRegistryConfiguration), out var value) ? value as ConsulRegistryConfiguration : default; protected IConsulClient Client => _client ??= _clientFactory.Get(Configuration); protected IOcelotLogger Logger => _logger ??= _loggerFactory.CreateLogger(); public virtual bool IsValid(ServiceEntry entry) { var service = entry.Service; var address = service.Address; bool valid = !string.IsNullOrEmpty(address) && !address.StartsWith(Uri.UriSchemeHttp + "://", StringComparison.OrdinalIgnoreCase) && !address.StartsWith(Uri.UriSchemeHttps + "://", StringComparison.OrdinalIgnoreCase) && service.Port > 0; if (!valid) { Logger.LogWarning( () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); } return valid; } public virtual IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) { ArgumentNullException.ThrowIfNull(entries); var services = new List(entries.Length); foreach (var serviceEntry in entries) { if (IsValid(serviceEntry)) { var serviceNode = GetNode(serviceEntry, nodes); var item = CreateService(serviceEntry, serviceNode); if (item != null) { services.Add(item); } } } return services; } protected virtual Node GetNode(ServiceEntry entry, Node[] nodes) => entry?.Node ?? nodes?.FirstOrDefault(n => n.Address == entry?.Service?.Address); public virtual Service CreateService(ServiceEntry entry, Node node) => new( GetServiceName(entry, node), GetServiceHostAndPort(entry, node), GetServiceId(entry, node), GetServiceVersion(entry, node), GetServiceTags(entry, node) ); protected virtual string GetServiceName(ServiceEntry entry, Node node) => entry.Service.Service; protected virtual ServiceHostAndPort GetServiceHostAndPort(ServiceEntry entry, Node node) => new( GetDownstreamHost(entry, node), entry.Service.Port); protected virtual string GetDownstreamHost(ServiceEntry entry, Node node) => node != null ? node.Name : entry.Service.Address; protected virtual string GetServiceId(ServiceEntry entry, Node node) => entry.Service.ID; protected virtual string GetServiceVersion(ServiceEntry entry, Node node) => entry.Service.Tags ?.FirstOrDefault(tag => tag.StartsWith(VersionPrefix, StringComparison.Ordinal)) ?.TrimPrefix(VersionPrefix) ?? string.Empty; protected virtual IEnumerable GetServiceTags(ServiceEntry entry, Node node) => entry.Service.Tags ?? Enumerable.Empty(); private const string VersionPrefix = "version-"; } ================================================ FILE: src/Ocelot.Provider.Consul/Interfaces/IConsulClientFactory.cs ================================================ namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulClientFactory { IConsulClient Get(ConsulRegistryConfiguration config); } ================================================ FILE: src/Ocelot.Provider.Consul/Interfaces/IConsulServiceBuilder.cs ================================================ using Ocelot.Values; namespace Ocelot.Provider.Consul.Interfaces; public interface IConsulServiceBuilder { // Keep config private (deep encapsulation) until an architectural decision is made. // ConsulRegistryConfiguration Configuration { get; } bool IsValid(ServiceEntry entry); IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes); Service CreateService(ServiceEntry serviceEntry, Node serviceNode); } ================================================ FILE: src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj ================================================  net8.0;net9.0;net10.0 disable disable true Provides Ocelot extensions to use Consul service discovery. 0.0.0-dev Ocelot.Provider.Consul Ocelot.Provider.Consul API Gateway;.NET;Consul https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Consul https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png win-x64;osx-x64 false false True false Tom Pallister, Raman Maksimchuk, Guillaume Gnaegi ..\..\codeanalysis.ruleset True 1591 Three Mammals Ocelot Gateway © 2025 Three Mammals. MIT licensed OSS ocelot_icon.png https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: src/Ocelot.Provider.Consul/OcelotBuilderExtensions.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul.Interfaces; namespace Ocelot.Provider.Consul; public static class OcelotBuilderExtensions { /// /// Integrates Consul service discovery into the DI, atop the existing Ocelot services. /// /// /// Default services: /// /// The service is an instance of . /// The service is an instance of . /// /// /// The Ocelot Builder instance, default. /// The reference to the same extended object. public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) { builder.Services .AddSingleton(ConsulProviderFactory.Get) .AddSingleton() .AddScoped() .RemoveAll(typeof(IFileConfigurationPollerOptions)) .AddSingleton(); return builder; } /// /// Integrates Consul service discovery into the DI, atop the existing Ocelot services, with service builder overriding. /// /// /// Services to override: /// /// The service has been substituted with a instance. /// /// /// The service builder type. /// The Ocelot Builder instance, default. /// The reference to the same extended object. public static IOcelotBuilder AddConsul(this IOcelotBuilder builder) where TServiceBuilder : class, IConsulServiceBuilder { AddConsul(builder).Services .RemoveAll() .AddScoped(typeof(IConsulServiceBuilder), typeof(TServiceBuilder)); return builder; } public static IOcelotBuilder AddConfigStoredInConsul(this IOcelotBuilder builder) { builder.Services .AddSingleton(ConsulMiddlewareConfigurationProvider.Get) .AddHostedService() .AddSingleton(); return builder; } } ================================================ FILE: src/Ocelot.Provider.Consul/PollConsul.cs ================================================ using Ocelot.Logging; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.Provider.Consul; public sealed class PollConsul : IServiceDiscoveryProvider { private readonly object _lockObject = new(); private readonly IOcelotLogger _logger; private readonly IServiceDiscoveryProvider _consulServiceDiscoveryProvider; private readonly int _pollingInterval; private DateTime _lastUpdateTime; private List _services; public PollConsul(int pollingInterval, string serviceName, IOcelotLoggerFactory factory, IServiceDiscoveryProvider consulServiceDiscoveryProvider) { _logger = factory.CreateLogger(); _consulServiceDiscoveryProvider = consulServiceDiscoveryProvider; _pollingInterval = pollingInterval; // Initialize by DateTime.MinValue as lowest value. // Polling will occur immediately during the first call _lastUpdateTime = DateTime.MinValue; _services = new List(); ServiceName = serviceName; } public string ServiceName { get; } /// /// Gets the services. /// If the first call, retrieves the services and then starts the timer. /// /// A with a result of . public Task> GetAsync() { lock (_lockObject) { var refreshTime = _lastUpdateTime.AddMilliseconds(_pollingInterval); // Check if any services available if (refreshTime >= DateTime.UtcNow && _services.Any()) { return Task.FromResult(_services); } try { _logger.LogInformation(() => $"Retrieving new client information for service: {ServiceName}..."); _services = _consulServiceDiscoveryProvider.GetAsync().GetAwaiter().GetResult(); return Task.FromResult(_services); } finally { _lastUpdateTime = DateTime.UtcNow; } } } } ================================================ FILE: src/Ocelot.Provider.Consul/UnableToSetConfigInConsulError.cs ================================================ using Ocelot.Errors; using HttpStatus = System.Net.HttpStatusCode; namespace Ocelot.Provider.Consul; public class UnableToSetConfigInConsulError : Error { public UnableToSetConfigInConsulError(string s) : base(s, OcelotErrorCode.UnknownError, (int)HttpStatus.NotFound) { } } ================================================ FILE: src/Ocelot.Provider.Consul/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.Linq; global using System.Threading.Tasks; // Project extra global namespaces global using Consul; global using Ocelot.ServiceDiscovery; ================================================ FILE: src/Ocelot.Provider.Consul/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net10.0/osx-x64": {}, "net10.0/win-x64": {}, "net8.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net8.0/osx-x64": {}, "net8.0/win-x64": {}, "net9.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net9.0/osx-x64": {}, "net9.0/win-x64": {} } } ================================================ FILE: src/Ocelot.Provider.Eureka/Eureka.cs ================================================ using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.Provider.Eureka; public class Eureka : IServiceDiscoveryProvider { private readonly string _serviceName; private readonly IDiscoveryClient _client; public Eureka(string serviceName, IDiscoveryClient client) { _serviceName = serviceName ?? throw new ArgumentNullException(nameof(serviceName)); _client = client ?? throw new ArgumentNullException(nameof(client)); } public Task> GetAsync() { var services = new List(); var instances = _client.GetInstances(_serviceName); if (instances != null && instances.Any()) { services.AddRange(instances.Select(i => new Service(i.ServiceId, new ServiceHostAndPort(i.Host, i.Port, i.Uri.Scheme), string.Empty, string.Empty, new List()))); } return Task.FromResult(services); } } ================================================ FILE: src/Ocelot.Provider.Eureka/EurekaMiddlewareConfigurationProvider.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Repository; using Ocelot.Middleware; namespace Ocelot.Provider.Eureka; public class EurekaMiddlewareConfigurationProvider { public static OcelotMiddlewareConfigurationDelegate Get { get; } = builder => { var internalConfigRepo = builder.ApplicationServices.GetService(); var config = internalConfigRepo.Get(); if (UsingEurekaServiceDiscoveryProvider(config.Data)) { //builder.UseDiscoveryClient(); } return Task.CompletedTask; }; private static bool UsingEurekaServiceDiscoveryProvider(IInternalConfiguration configuration) { return configuration?.ServiceProviderConfiguration != null && configuration.ServiceProviderConfiguration.Type?.ToLower() == "eureka"; } } ================================================ FILE: src/Ocelot.Provider.Eureka/EurekaProviderFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.Provider.Eureka; public static class EurekaProviderFactory { /// /// String constant used for provider type definition. /// public const string Eureka = nameof(Provider.Eureka.Eureka); public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { var client = provider.GetService(); if (client == null) { throw new NullReferenceException($"Cannot get an {nameof(IDiscoveryClient)} service during {nameof(CreateProvider)} operation to instanciate the {nameof(Eureka)} provider!"); } return Eureka.Equals(config.Type, StringComparison.OrdinalIgnoreCase) ? new Eureka(route.ServiceName, client) : null; } } ================================================ FILE: src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj ================================================  net8.0;net9.0;net10.0 disable disable true Provides Ocelot extensions to use Netflix Eureka service discovery. 0.0.0-dev Ocelot.Provider.Eureka Ocelot.Provider.Eureka API Gateway;.NET;Netflix;Eureka https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Eureka https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png win-x64;osx-x64 false false True false Tom Pallister, Raman Maksimchuk ..\..\codeanalysis.ruleset True 1591 Three Mammals Ocelot Gateway © 2026 Three Mammals. MIT licensed OSS ocelot_icon.png https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: src/Ocelot.Provider.Eureka/OcelotBuilderExtensions.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Steeltoe.Discovery.Client; namespace Ocelot.Provider.Eureka; public static class OcelotBuilderExtensions { public static IOcelotBuilder AddEureka(this IOcelotBuilder builder) { builder.Services .AddDiscoveryClient(builder.Configuration) .AddSingleton(EurekaProviderFactory.Get) .AddSingleton(EurekaMiddlewareConfigurationProvider.Get); return builder; } } ================================================ FILE: src/Ocelot.Provider.Eureka/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using Ocelot; global using Ocelot.ServiceDiscovery; global using Steeltoe.Discovery; global using Steeltoe.Discovery.Eureka; ================================================ FILE: src/Ocelot.Provider.Eureka/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net10.0/osx-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } }, "net10.0/win-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } }, "net8.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net8.0/osx-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } }, "net8.0/win-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } }, "net9.0": { "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "plvZ0ZIpq+97gdPNNvhwvrEZ92kNml9hd1pe3idMA7svR0PztdzVLkoWLcRFgySYXUJc3kSM3Xw3mNFMo/bxRA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "McP+Lz/EKwvtCv48z0YImw+L1gi1gy5rHhNaNIY2CrjloV+XY8gydT8DjMR6zWeL13AFK+DioVpppwAuO1Gi1w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "C2wqUoh9OmRL1akaCcKSTmRU8z0kckfImG7zLNI8uyi47Lp+zd5LWAD17waPQEqCz3ioWOCrFUo+JJuoeZLOBw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "UboiXxpPUpwulHvIAVE36Knq0VSHaAmfrFkegLyBZeaADuKezJ/AIXYAW8F5GBlGk/VaibN2k/Zn1ca8YAfVdA==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileSystemGlobbing": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "OK+670i7esqlQrPjdIKRbsyMCe9g5kSLpRRQGSr4Q58AOYEe/hCnfLZprh7viNisSUUQZmMrbbuDaIrP+V1ebQ==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "3.1.0", "contentHash": "DLigdcV0nYaT6/ly0rnfP80BnXq8NNd/h8/SkfY39uio7Bd9LauVntp6RcRh1Kj23N+uf80GgL7Win6P3BCtoQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.0", "Microsoft.Extensions.Logging": "3.1.0", "Microsoft.Extensions.Options": "3.1.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Logging.Console": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "e+48o7DztoYog+PY430lPxrM4mm3PbA6qucvQtUDDwVo4MO+ejMw7YGc/o2rnxbxj4isPxdfKFzTxvXMwAz83A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "dt0x21qBdudHLW/bjMJpkixv858RRr8eSomgVbU8qljOyfrfDGi1JQvpF9w8S7ziRPtRKisuWaOwFxJM82GxeA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net9.0/osx-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } }, "net9.0/win-x64": { "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" } } } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/EndPointClientV1.cs ================================================ using KubeClient.Http; using KubeClient.Models; using KubeClient.ResourceClients; using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes; public class EndPointClientV1 : KubeResourceClient, IEndPointClient { private static readonly HttpRequest EndpointsRequest = KubeRequest.Create("api/v1/namespaces/{Namespace}/endpoints/{ServiceName}"); private static readonly HttpRequest EndpointsWatchRequest = KubeRequest.Create("api/v1/watch/namespaces/{Namespace}/endpoints/{ServiceName}"); public EndPointClientV1(IKubeApiClient client) : base(client) { } public Task GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(serviceName); var request = EndpointsRequest.WithTemplateParameters(new { Namespace = kubeNamespace ?? KubeClient.DefaultNamespace, ServiceName = serviceName, }); return Http.GetAsync(request, cancellationToken) .ReadContentAsObjectV1Async(operationDescription: $"{nameof(GetAsync)} {nameof(EndpointsV1)}"); } public IObservable> Watch(string serviceName, string kubeNamespace, CancellationToken cancellationToken = default) { ArgumentException.ThrowIfNullOrEmpty(serviceName); var request = EndpointsWatchRequest.WithTemplateParameters(new { ServiceName = serviceName, Namespace = kubeNamespace ?? KubeClient.DefaultNamespace, }); return ObserveEvents(request, $"{nameof(Watch)} {nameof(EndpointsV1)} for '{serviceName}' in the namespace '{kubeNamespace ?? KubeClient.DefaultNamespace}'"); } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Interfaces/IEndPointClient.cs ================================================ using KubeClient.Models; using KubeClient.ResourceClients; namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IEndPointClient : IKubeResourceClient { Task GetAsync(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default); IObservable> Watch(string serviceName, string kubeNamespace = null, CancellationToken cancellationToken = default); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Interfaces/IKubeApiClientFactory.cs ================================================ namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IKubeApiClientFactory { KubeApiClient Get(bool usePodServiceAccount); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceBuilder.cs ================================================ using KubeClient.Models; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IKubeServiceBuilder { IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Interfaces/IKubeServiceCreator.cs ================================================ using KubeClient.Models; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes.Interfaces; public interface IKubeServiceCreator { IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset); IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Kube.cs ================================================ using KubeClient.Models; using Ocelot.Infrastructure.DesignPatterns; using Ocelot.Logging; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; /// Default Kubernetes service discovery provider. /// /// /// NuGet: KubeClient /// GitHub: dotnet-kube-client /// /// public class Kube : IServiceDiscoveryProvider, IDisposable { private static readonly (string ResourceKind, string ResourceApiVersion) EndPointsKubeKind = KubeObjectV1.GetKubeKind(); private readonly KubeRegistryConfiguration _configuration; private readonly IOcelotLogger _logger; private readonly IKubeApiClient _kubeApi; private readonly IKubeServiceBuilder _serviceBuilder; private bool _disposed; public Kube( KubeRegistryConfiguration configuration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi, IKubeServiceBuilder serviceBuilder) { _configuration = configuration; _logger = factory.CreateLogger(); _kubeApi = kubeApi; _serviceBuilder = serviceBuilder; } public virtual async Task> GetAsync() { if (_disposed) return new(0); var endpoint = await Retry.OperationAsync(GetEndpoint, CheckErroneousState, logger: _logger); if (CheckErroneousState(endpoint)) { _logger.LogWarning(() => GetMessage($"Unable to use bad result returned by {nameof(Kube)} integration endpoint because the final result is invalid/unknown after multiple retries!")); return new(0); } return BuildServices(_configuration, endpoint) .ToList(); } private string Message(string details) => $"Failed to retrieve {EndPointsKubeKind.ResourceApiVersion}/{EndPointsKubeKind.ResourceKind} '{_configuration.KeyOfServiceInK8s}' in namespace '{_configuration.KubeNamespace}': {details}"; private async Task GetEndpoint() { if (_disposed) return null; try { return await _kubeApi .EndpointsV1() .GetAsync(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace); } catch (KubeApiException ex) { string Msg() { StatusV1 status = ex.Status; string httpStatusCode = "-"; // Unknown if (ex.InnerException is HttpRequestException e) { httpStatusCode = e.StatusCode.ToString(); } return Message($"(HTTP.{httpStatusCode}/{status.Status}/{status.Reason}): {status.Message}"); } _logger.LogError(Msg, ex); } catch (HttpRequestException ex) { _logger.LogError(() => Message($"({ex.HttpRequestError}/HTTP.{ex.StatusCode})."), ex); } catch (Exception unexpected) { _logger.LogError(() => Message($"(an unexpected ex occurred)."), unexpected); } return null; } private bool CheckErroneousState(EndpointsV1 endpoint) => (endpoint?.Subsets?.Count ?? 0) == 0; // null or count is zero private string GetMessage(string message) => $"{nameof(Kube)} provider. Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; {message}"; protected virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) => _disposed ? Enumerable.Empty() : _serviceBuilder.BuildServices(configuration, endpoint); protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _logger?.Dispose(); _kubeApi?.Dispose(); } _disposed = true; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubeApiClientExtensions.cs ================================================ using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes; public static class KubeApiClientExtensions { public static IEndPointClient EndpointsV1(this IKubeApiClient client) => client.ResourceClient(x => new EndPointClientV1(x)); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubeApiClientFactory.cs ================================================ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Provider.Kubernetes.Interfaces; namespace Ocelot.Provider.Kubernetes; public class KubeApiClientFactory : IKubeApiClientFactory { private readonly ILoggerFactory _logger; private readonly IOptions _options; private string _serviceAccountPath; public KubeApiClientFactory(ILoggerFactory logger, IOptions options) { _logger = logger; _options = options; _serviceAccountPath = KubeClientConstants.DefaultServiceAccountPath; } protected string ServiceAccountPath { get => _serviceAccountPath; set => _serviceAccountPath = string.IsNullOrEmpty(value) ? KubeClientConstants.DefaultServiceAccountPath : value; } public virtual KubeApiClient Get(bool usePodServiceAccount) { var options = usePodServiceAccount ? KubeClientOptions.FromPodServiceAccount(ServiceAccountPath) : _options.Value; options.LoggerFactory = _logger; return KubeApiClient.Create(options); } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubeRegistryConfiguration.cs ================================================ namespace Ocelot.Provider.Kubernetes; public class KubeRegistryConfiguration { public string KubeNamespace { get; set; } public string KeyOfServiceInK8s { get; set; } public string Scheme { get; set; } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubeServiceBuilder.cs ================================================ using KubeClient.Models; using Ocelot.Logging; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; public class KubeServiceBuilder : IKubeServiceBuilder { private readonly IOcelotLogger _logger; private readonly IKubeServiceCreator _serviceCreator; public KubeServiceBuilder(IOcelotLoggerFactory factory, IKubeServiceCreator serviceCreator) { ArgumentNullException.ThrowIfNull(factory); _logger = factory.CreateLogger(); ArgumentNullException.ThrowIfNull(serviceCreator); _serviceCreator = serviceCreator; } public virtual IEnumerable BuildServices(KubeRegistryConfiguration configuration, EndpointsV1 endpoint) { ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(endpoint); var services = endpoint.Subsets .SelectMany(subset => _serviceCreator.Create(configuration, endpoint, subset)) .ToArray(); _logger.LogDebug(() => $"K8s '{Check(endpoint.Kind)}:{Check(endpoint.ApiVersion)}:{Check(endpoint.Metadata?.Name)}' endpoint: Total built {services.Length} services."); return services; } private static string Check(string str) => string.IsNullOrEmpty(str) ? "?" : str; } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubeServiceCreator.cs ================================================ using KubeClient.Models; using Ocelot.Logging; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.Provider.Kubernetes; public class KubeServiceCreator : IKubeServiceCreator { public KubeServiceCreator(IOcelotLoggerFactory factory) { ArgumentNullException.ThrowIfNull(factory); Logger = factory.CreateLogger(); } public virtual IEnumerable Create(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset) => (configuration == null || endpoint == null || subset == null) ? Array.Empty() : subset.Addresses .SelectMany(address => CreateInstance(configuration, endpoint, subset, address)) .ToArray(); public virtual IEnumerable CreateInstance(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) { var instance = new Service( GetServiceName(configuration, endpoint, subset, address), GetServiceHostAndPort(configuration, endpoint, subset, address), GetServiceId(configuration, endpoint, subset, address), GetServiceVersion(configuration, endpoint, subset, address), GetServiceTags(configuration, endpoint, subset, address) ); return new Service[] { instance }; } protected IOcelotLogger Logger { get; } protected virtual string GetServiceName(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) => endpoint.Metadata?.Name; protected virtual ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) { var ports = subset.Ports; bool portNameToScheme(EndpointPortV1 p) => string.Equals(p.Name, configuration.Scheme, StringComparison.OrdinalIgnoreCase); var portV1 = string.IsNullOrEmpty(configuration.Scheme) || !ports.Any(portNameToScheme) ? ports.FirstOrDefault() : ports.FirstOrDefault(portNameToScheme); portV1 ??= new(); portV1.Name ??= configuration.Scheme ?? string.Empty; Logger.LogDebug(() => $"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}]."); return new ServiceHostAndPort(address.Ip, portV1.Port ?? 80, portV1.Name); } protected virtual string GetServiceId(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) => endpoint.Metadata?.Uid; protected virtual string GetServiceVersion(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) => endpoint.ApiVersion; protected virtual IEnumerable GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) => Enumerable.Empty(); } ================================================ FILE: src/Ocelot.Provider.Kubernetes/KubernetesProviderFactory.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Provider.Kubernetes.Interfaces; //using System.Collections.Concurrent; using System.Reactive.Concurrency; namespace Ocelot.Provider.Kubernetes; public static class KubernetesProviderFactory // TODO : IServiceDiscoveryProviderFactory { /// String constant used for provider type definition. public const string PollKube = nameof(Kubernetes.PollKube); public const string WatchKube = nameof(Kubernetes.WatchKube); // private static readonly ConcurrentDictionary _providers = new(); // TODO It must be singleton service in DI-container public static ServiceDiscoveryFinderDelegate Get { get; } = CreateProvider; private static IServiceDiscoveryProvider CreateProvider(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute route) { //if (_providers.TryGetValue(route.LoadBalancerKey, out var instance)) // ?? route.ServiceName ?? // return instance; var factory = provider.GetService(); var kubeClient = provider.GetService(); var serviceBuilder = provider.GetService(); var configuration = new KubeRegistryConfiguration { KeyOfServiceInK8s = route.ServiceName, KubeNamespace = string.IsNullOrEmpty(route.ServiceNamespace) ? config.Namespace : route.ServiceNamespace, Scheme = route.DownstreamScheme, }; if (WatchKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase)) { //return _providers.GetOrAdd(route.LoadBalancerKey, // key => new WatchKube(configuration, factory, kubeClient, serviceBuilder, Scheduler.Default)); return new WatchKube(configuration, factory, kubeClient, serviceBuilder, Scheduler.Default); } var kubeProvider = new Kube(configuration, factory, kubeClient, serviceBuilder); return /*_providers.GetOrAdd(route.LoadBalancerKey, key =>*/ PollKube.Equals(config.Type, StringComparison.OrdinalIgnoreCase) ? new PollKube(config.PollingInterval, factory, kubeProvider) : kubeProvider; //); } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/ObservableExtensions.cs ================================================ using System.Reactive.Concurrency; using System.Reactive.Linq; namespace Ocelot.Provider.Kubernetes; public static class ObservableExtensions { public static IObservable RetryAfter(this IObservable source, TimeSpan dueTime, IScheduler scheduler) => RepeatInfinite(source, dueTime, scheduler).Catch(); private static IEnumerable> RepeatInfinite(IObservable source, TimeSpan dueTime, IScheduler scheduler) { yield return source; while (true) { yield return source.DelaySubscription(dueTime, scheduler); } } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj ================================================  net8.0;net9.0;net10.0 disable disable true Ocelot Gateway Provides Ocelot extensions to use Kubernetes service discovery. 0.0.0-dev Ocelot.Provider.Kubernetes API Gateway;.NET;kubernetes https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Kubernetes https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png Ocelot.Provider.Kubernetes win-x64;osx-x64 false false true false Tom Pallister, Raman Maksimchuk Three Mammals ..\..\codeanalysis.ruleset True 1591 © 2026 Three Mammals. MIT licensed OSS ocelot_icon.png https://github.com/ThreeMammals/Ocelot.git LICENSE.md ================================================ FILE: src/Ocelot.Provider.Kubernetes/OcelotBuilderExtensions.cs ================================================ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Kubernetes.Interfaces; using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; namespace Ocelot.Provider.Kubernetes; public static class OcelotBuilderExtensions { /// /// Adds Kubernetes (K8s) services with or without using a pod service account. /// By default, is set to , which means using a pod service account. /// /// If is , it internally injects an configuration section object (where TOptions is ) to configure . /// The Ocelot Builder instance. /// If true, it creates the client from pod service account. /// The reference to the same extended object. public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, bool usePodServiceAccount = true) { builder.Services //.AddSingleton(KubeApiClientFactory) // TODO Revert to .AddKubeClient(usePodServiceAccount) after making KubernetesProviderFactory as IServiceDiscoveryProviderFactory //.AddKubeApiClientFactory(usePodServiceAccount) //.AddKubeClient(usePodServiceAccount) .AddSingleton() .AddSingleton(ResolveWithKubeApiClientFactory) .AddSingleton(KubernetesProviderFactory.Get) // TODO Must be removed after deprecation of ServiceDiscoveryFinderDelegate in favor of IServiceDiscoveryProviderFactory .AddSingleton() .AddSingleton(); return builder; KubeApiClient ResolveWithKubeApiClientFactory(IServiceProvider sp) { var factory = sp.GetService(); return factory.Get(usePodServiceAccount); } } /// /// Adds Kubernetes (K8s) services without using a pod service account, explicitly calling a factory-action to initialize configuration options for . /// Before adding services, it internally configures options by registering the action in DI; thus an (where TOptions is ) object becomes available in the DI container. /// /// It operates in 2 modes: /// /// If is provided (action is not null), it calls the action ignoring all optional arguments. /// If is not provided (action is null), it reads the global options and reuses them to initialize the following properties: , , and , finally initializing the rest of the properties with optional arguments. /// /// /// The Ocelot Builder instance. /// An action to initialize of the client. It can be null: read the remarks. /// Optional scheme to build URI when the global is unknown, defaulting to 'https' aka . /// Optional host to build URI when the global is unknown, defaulting to 'localhost' aka . /// Optional port to build URI when the global is unknown, defaulting to 443. /// Optional namespace to initialize option when the global is unknown, defaulting to 'default'. /// Optional username to initialize the option. /// Optional password to initialize the option. /// Optional command to initialize the option. /// Optional arguments to initialize the option. /// Optional selector to initialize the option. /// Optional selector to initialize the option. /// Optional token to initialize the option. /// Optional date-time to initialize the option. /// Optional certificate to initialize the option. /// Optional certificate to initialize the option. /// Optional verification flag to initialize the option, defaulting to false. /// Optional strategy to initialize the option, defaulting to . /// Optional log flag to initialize the option, defaulting to false. /// Optional log flag to initialize the option, defaulting to false. /// Optional factory to initialize the option. /// Optional list to add assemblies to the option, defaulting to empty list. /// Optional dictionary to initialize the option, defaulting to empty list. /// The reference to the same extended object. public static IOcelotBuilder AddKubernetes(this IOcelotBuilder builder, Action configureOptions, // required params string defaultScheme = null, string defaultHost = null, int? defaultPort = null, string defaultNamespace = null, // optional params string username = null, string password = null, string accessTokenCommand = null, string accessTokenCommandArguments = null, string accessTokenSelector = null, string accessTokenExpirySelector = null, string initialAccessToken = null, DateTime? initialTokenExpiryUtc = null, X509Certificate2 clientCertificate = null, X509Certificate2 certificationAuthorityCertificate = null, bool? allowInsecure = null, KubeAuthStrategy? authStrategy = null, bool? logHeaders = null, bool? logPayloads = null, ILoggerFactory loggerFactory = null, List modelTypeAssemblies = null, Dictionary environmentVariables = null) { configureOptions ??= Configure; builder.Services.AddOptions().Configure(configureOptions); return builder.AddKubernetes(false); void Configure(KubeClientOptions options) { // Initialize properties with values coming from global ServiceDiscoveryProvider options var key = $"{nameof(FileConfiguration.GlobalConfiguration)}:{nameof(FileGlobalConfiguration.ServiceDiscoveryProvider)}"; var section = builder.Configuration.GetSection(key); var scheme = section.Str(nameof(FileServiceDiscoveryProvider.Scheme), defaultScheme ?? Uri.UriSchemeHttps); var host = section.Str(nameof(FileServiceDiscoveryProvider.Host), defaultHost ?? IPAddress.Loopback.ToString()); var port = section.Int(nameof(FileServiceDiscoveryProvider.Port), defaultPort ?? 443); options.ApiEndPoint = new UriBuilder(scheme, host, port).Uri; options.KubeNamespace = section.Str(nameof(FileServiceDiscoveryProvider.Namespace), defaultNamespace ?? "default"); options.AccessToken = section.GetValue(nameof(FileServiceDiscoveryProvider.Token)); // Initialize properties with values coming from optional arguments options.AuthStrategy = authStrategy ?? KubeAuthStrategy.BearerToken; options.AllowInsecure = allowInsecure ?? false; options.AccessTokenCommand = accessTokenCommand; options.AccessTokenCommandArguments = accessTokenCommandArguments; options.AccessTokenExpirySelector = accessTokenExpirySelector; options.AccessTokenSelector = accessTokenSelector; options.CertificationAuthorityCertificate = certificationAuthorityCertificate; options.ClientCertificate = clientCertificate; options.EnvironmentVariables = environmentVariables ?? new(); options.InitialAccessToken = initialAccessToken; options.InitialTokenExpiryUtc = initialTokenExpiryUtc; options.LoggerFactory = loggerFactory; options.LogHeaders = logHeaders ?? false; options.LogPayloads = logPayloads ?? false; options.ModelTypeAssemblies.AddRange(modelTypeAssemblies ?? new()); options.Password = password; options.Username = username; } } private static string Str(this IConfigurationSection sec, string key, string defaultValue) { string val = sec.GetValue(key, defaultValue); return string.IsNullOrEmpty(val) ? defaultValue : val; } private static int Int(this IConfigurationSection sec, string key, int defaultValue) { int val = sec.GetValue(key, defaultValue); return val <= 0 ? defaultValue : val; } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/PollKube.cs ================================================ using Ocelot.Logging; using Ocelot.Values; using System.Collections.Concurrent; using YamlDotNet.Core.Tokens; namespace Ocelot.Provider.Kubernetes; /// /// It polls the provider in the specified intervals and update the queue with new versions of services. /// public class PollKube : IServiceDiscoveryProvider, IDisposable { private readonly IOcelotLogger _logger; private readonly IServiceDiscoveryProvider _provider; // TODO IDisposable private readonly ConcurrentQueue> _queue = new(); public static readonly List Empty = new(0); private Task _timing; private PeriodicTimer _timer; private CancellationTokenSource _cts = new(); private volatile bool _polling, _disposed, _stopped; public PollKube(int pollingInterval, IOcelotLoggerFactory factory, IServiceDiscoveryProvider kubeProvider) { _logger = factory.CreateLogger(); _provider = kubeProvider; _timer = new(TimeSpan.FromMilliseconds(pollingInterval)); } public async Task> GetAsync() { _timing ??= StartAsync(); // (_cts.Token); if (_disposed || _cts.IsCancellationRequested) return Empty; // First cold request must call the provider if (_queue.IsEmpty) { return await PollAsync(_cts.Token); } else if (_polling && _queue.TryPeek(out var oldVersion)) { return oldVersion; } // For services with multiple versions, remove outdated versions and retain only the latest one while (!_polling && _queue.Count > 1 && _queue.TryDequeue(out _)) { } _queue.TryPeek(out var latestVersion); return latestVersion; } protected virtual async Task> PollAsync(CancellationToken token) { if (_disposed || token.IsCancellationRequested) return Empty; // Avoid polling if already in progress due to a slow completion of the PollAsync task, // and ensure no more than three versions of services remain in the queue. if (_polling || _queue.Count > 3) return Empty; // but don't enqueue try { _polling = true; var services = await _provider.GetAsync(); // TODO Add cancellation if (_disposed || token.IsCancellationRequested) return Empty; _queue.Enqueue(services); return services; } catch (ObjectDisposedException) { return Empty; } finally { _polling = false; } } /// /// Endless task which should be stopped during disposing or when the provider is no longer needed. /// protected async Task StartAsync() { try { while (!_disposed && !_stopped && await _timer.WaitForNextTickAsync(_cts.Token)) { await PollAsync(_cts.Token); } } catch (OperationCanceledException) { // Expected on cancellation aka _cts.Cancel() } finally { _queue.Clear(); } } protected void Stop() { if (_disposed) return; _cts.Cancel(); _timer?.Dispose(); _stopped = true; // the flag ensures the loop will exit _timing?.GetAwaiter().GetResult(); // due to the flag this wait should complete in a reasonable time, in polling interval at most _timing?.Dispose(); _cts.Dispose(); } #region Dispose pattern public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { Stop(); _logger?.Dispose(); } //_cts = null; _timer = null; _timing = null; _disposed = true; } ~PollKube() => Dispose(false); #endregion } ================================================ FILE: src/Ocelot.Provider.Kubernetes/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using KubeClient; global using Ocelot; global using Ocelot.ServiceDiscovery; global using Ocelot.ServiceDiscovery.Providers; ================================================ FILE: src/Ocelot.Provider.Kubernetes/WatchKube.cs ================================================ using KubeClient.Models; using Ocelot.Logging; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; using System.Reactive.Concurrency; using System.Reactive.Linq; namespace Ocelot.Provider.Kubernetes; public class WatchKube : IServiceDiscoveryProvider, IDisposable { /// The default number of seconds to wait before scheduling the next retry for the subscription operation. /// A positive integer that is greater than or equal to 1. public static int FailedSubscriptionRetrySeconds { get => failedSubscriptionRetrySeconds; set => failedSubscriptionRetrySeconds = value >= 1 ? value : 1; } /// The default number of seconds to wait after Ocelot starts, following the provider's creation, to fetch the first result from the Kubernetes endpoint. /// A positive integer that is greater than or equal to 1. public static int FirstResultsFetchingTimeoutSeconds { get => firstResultsFetchingTimeoutSeconds; set => firstResultsFetchingTimeoutSeconds = value >= 1 ? value : 1; } private static int failedSubscriptionRetrySeconds = 1; private static int firstResultsFetchingTimeoutSeconds = 1; private readonly KubeRegistryConfiguration _configuration; private readonly IOcelotLogger _logger; private readonly IKubeApiClient _kubeApi; private readonly IKubeServiceBuilder _serviceBuilder; private readonly IScheduler _scheduler; private readonly IDisposable _subscription; private TaskCompletionSource _firstResultsCompletionSource; private List _services = new(); public WatchKube( KubeRegistryConfiguration configuration, IOcelotLoggerFactory factory, IKubeApiClient kubeApi, IKubeServiceBuilder serviceBuilder, IScheduler scheduler) { _configuration = configuration; _logger = factory.CreateLogger(); _kubeApi = kubeApi; _serviceBuilder = serviceBuilder; _scheduler = scheduler; SetFirstResultsCompletedAfterDelay(); _subscription = CreateSubscription(); } public virtual async Task> GetAsync() { // Wait for first results fetching await _firstResultsCompletionSource.Task; if (_services.Count == 0) { _logger.LogWarning(() => GetMessage("Subscription to service endpoints gave no results!")); } return _services; } private void SetFirstResultsCompletedAfterDelay() { _firstResultsCompletionSource = new(); Observable .Timer(TimeSpan.FromSeconds(FirstResultsFetchingTimeoutSeconds), _scheduler) .Subscribe(_ => _firstResultsCompletionSource.TrySetResult()); } private void OnNext(IResourceEventV1 endpointEvent) { _services = endpointEvent.EventType switch { ResourceEventType.Deleted or ResourceEventType.Error => new(), _ when (endpointEvent.Resource?.Subsets.Count ?? 0) == 0 => new(), _ => _serviceBuilder.BuildServices(_configuration, endpointEvent.Resource).ToList(), }; _firstResultsCompletionSource.TrySetResult(); } // Called only when subscription canceled in Dispose private void OnCompleted() => _logger.LogInformation(() => GetMessage("Subscription to service endpoints completed")); private void OnException(Exception ex) => _logger.LogError(() => GetMessage("Endpoints subscription error occured."), ex); private IDisposable CreateSubscription() => _kubeApi .EndpointsV1() .Watch(_configuration.KeyOfServiceInK8s, _configuration.KubeNamespace) .Do(_ => { }, OnException) .RetryAfter(TimeSpan.FromSeconds(FailedSubscriptionRetrySeconds), _scheduler) .Subscribe(OnNext, OnCompleted); private string GetMessage(string message) => $"{nameof(WatchKube)} provider. Namespace:{_configuration.KubeNamespace}, Service:{_configuration.KeyOfServiceInK8s}; {message}"; public void Dispose() { _subscription.Dispose(); GC.SuppressFinalize(this); } } ================================================ FILE: src/Ocelot.Provider.Kubernetes/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "KubeClient": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "tL9cSl3maS5FPzp/3MtlZI21ExWhni0nnUCF8HY4npTsINw45n9SNDbkKXBMtFyUFGSsQep25fHIDN4f/Vp3AQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net10.0/osx-x64": {}, "net10.0/win-x64": {}, "net8.0": { "KubeClient": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "8.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", "dependencies": { "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net8.0/osx-x64": {}, "net8.0/win-x64": {}, "net9.0": { "KubeClient": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Direct", "requested": "[3.1.1, )", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "9.0.3" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "RIEeZxWYm77+OWLwgik7DzSVSONjqkmcbuCb1koZdGAV7BgOUWnLz80VMyHZMw3onrVwFCCMHBBdruBPuQTvkg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Primitives": "9.0.3" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "q5qlbm6GRUrle2ZZxy9aqS/wWoc+mRD3JeP6rcpiJTh5XcemYkplAcJKq8lU11ZfPom5lfbZZfnQvDqcUhqD5Q==", "dependencies": { "Microsoft.Extensions.Primitives": "9.0.3" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "ad82pYBUSQbd3WIboxsS1HzFdRuHKRa2CpYwie/o6dZAxUjt62yFwjoVdM7Iw2VO5fHV1rJwa7jJZBNZin0E7Q==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "lDbxJpkl6X8KZGpkAxgrrthQ42YeiR0xjPp7KPx+sCPc3ZbpaIbjzd0QQ+9kDdK2RU2DOl3pc6tQyAgEZY3V0A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==", "dependencies": { "Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Diagnostics": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "utIi2R1nm+PCWkvWBf1Ou6LWqg9iLfHU23r8yyU9VCvda4dEs7xbTZSwGa5KuwbpzpgCbHCIuKaFHB3zyFmnGw==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "H/MBMLt9A/69Ux4OrV7oCKt3DcMT04o5SCqDolulzQA66TLFEpYYb4qedMs/uwrLtyHXGuDGWKZse/oa8W9AZw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "xE7MpY70lkw1oiid5y6FbL9dVw8oLfkx8RhSNGN8sSzBlCqGn0SyT3Fqc8tZnDaPIq7Z8R9RTKlS564DS+MV3g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Primitives": "9.0.3" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "PcyYHQglKnWVZHSPaL6v2qnfsIuFw8tSq7cyXHg3OeuDVn/CqmdWUjRiZomCF/Gi+qCi+ksz0lFphg2cNvB8zQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3", "Microsoft.Extensions.Primitives": "9.0.3" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "yCCJHvBcRyqapMSNzP+kTc57Eaavq2cr5Tmuil6/XVnipQf5xmskxakSQ1enU6S4+fNg3sJ27WcInV64q24JsA==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net9.0/osx-x64": {}, "net9.0/win-x64": {} } } ================================================ FILE: src/Ocelot.Provider.Polly/CircuitBreakerStrategy.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Provider.Polly; /// /// Polly requirements for the Circuit breaker resilience strategy. /// The subjects of this strategy are the and properties. /// public static class CircuitBreakerStrategy { // --- BreakDuration --- // Actual Polly's BreakDuration constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_BreakDuration public const int LowBreakDuration = 500; // 0.5 seconds public const int HighBreakDuration = 86_400_000; // 1 day, 24 hours in milliseconds /// Default duration of break the circuit will stay open before resetting, in milliseconds. public const int DefaultBreakDuration = 5_000; // 5 seconds /// /// Applies Polly's BreakDuration constraint to the value. /// If using Polly v8 or later, and in accordance with Polly's BreakDuration constraint, this value must be greater than (0.5 seconds) and less than (1 day). /// The value in milliseconds. /// The same value if the constraint is satisfied; otherwise, the default value (). public static int BreakDuration(int milliseconds) => IsValidBreakDuration(milliseconds) ? milliseconds : DefaultBreakDuration; public static bool IsValidBreakDuration(this int milliseconds) => milliseconds > LowBreakDuration && milliseconds < HighBreakDuration; // --- MinimumThroughput --- // Actual Polly's MinimumThroughput constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_MinimumThroughput public const int LowMinimumThroughput = 2; /// Default minimum throughput: this many actions or more must pass through the circuit in the time-slice, for statistics to be considered significant and the circuit-breaker to come into action. public const int DefaultMinimumThroughput = 100; /// /// Applies Polly's MinimumThroughput constraint to the value. /// If using Polly v8 or later, and in accordance with Polly's MinimumThroughput constraint, this value must be (2) or greater. /// The number of failures. /// The same value if the constraint is satisfied; otherwise, the default value (). public static int MinimumThroughput(int failures) => IsValidMinimumThroughput(failures) ? failures : DefaultMinimumThroughput; public static bool IsValidMinimumThroughput(this int failures) => failures >= LowMinimumThroughput; // --- FailureRatio --- // Actual Polly's FailureRatio constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_FailureRatio public const double LowFailureRatio = 0.0D; // ~ 0% public const double HighFailureRatio = 1.0D; // ~100% /// The FailureRatio default value is 0.1 (i.e. 10%). public const double DefaultFailureRatio = 0.1D; // ~10% /// /// Applies Polly's FailureRatio constraint to the value. /// If using Polly v8 or later, and in accordance with Polly's FailureRatio constraint, this value must be greater than (0) and less than (1). /// The value as quotient (~ percents). /// The same value if the constraint is satisfied; otherwise, the default value (). public static double FailureRatio(double ratio) => IsValidFailureRatio(ratio) ? ratio : DefaultFailureRatio; public static bool IsValidFailureRatio(this double ratio) => ratio > LowFailureRatio && ratio < HighFailureRatio; // --- SamplingDuration --- // Actual Polly's SamplingDuration constraint -> https://www.pollydocs.org/api/Polly.CircuitBreaker.CircuitBreakerStrategyOptions-1.html#Polly_CircuitBreaker_CircuitBreakerStrategyOptions_1_SamplingDuration public const int LowSamplingDuration = 500; // 0.5 seconds public const int HighSamplingDuration = 86_400_000; // 1 day, 24 hours in milliseconds /// The SamplingDuration default value is 30 seconds, in milliseconds. public const int DefaultSamplingDuration = 30_000; // 30 seconds /// /// Applies Polly's SamplingDuration constraint to the value. /// If using Polly v8 or later, and in accordance with Polly's SamplingDuration constraint, this value must be greater than (0.5 seconds) and less than (1 day). /// The value in milliseconds. /// The same value if the constraint is satisfied; otherwise, the default value (). public static int SamplingDuration(int milliseconds) => IsValidSamplingDuration(milliseconds) ? milliseconds : DefaultSamplingDuration; public static bool IsValidSamplingDuration(this int milliseconds) => milliseconds > LowSamplingDuration && milliseconds < HighSamplingDuration; } ================================================ FILE: src/Ocelot.Provider.Polly/Interfaces/IPollyQoSResiliencePipelineProvider.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Provider.Polly.Interfaces; /// Defines provider for Polly V8 pipelines. /// An HTTP result type, usually it is type. public interface IPollyQoSResiliencePipelineProvider where TResult : IDisposable { /// /// Gets Polly v8 pipeline. /// /// The route to apply a pipeline for. /// A object where T is . ResiliencePipeline GetResiliencePipeline(DownstreamRoute route); } ================================================ FILE: src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj ================================================  net8.0;net9.0;net10.0 disable disable true Provides Ocelot extensions to use Polly.NET 0.0.0-dev Ocelot.Provider.Polly Ocelot.Provider.Polly API Gateway;.NET;Polly https://github.com/ThreeMammals/Ocelot/tree/main/src/Ocelot.Provider.Polly https://raw.githubusercontent.com/ThreeMammals/Ocelot/assets/images/ocelot_icon_128x128.png win-x64;osx-x64 false false True false Tom Pallister, Raman Maksimchuk, Raynald Messié ..\..\codeanalysis.ruleset True 1591 Three Mammals Ocelot Gateway © 2026 Three Mammals. MIT licensed OSS ocelot_icon.png https://github.com/ThreeMammals/Ocelot.git LICENSE.md full True ================================================ FILE: src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; using Ocelot.QualityOfService; using Polly.CircuitBreaker; using Polly.Registry; using Polly.Timeout; namespace Ocelot.Provider.Polly; public static class OcelotBuilderExtensions { /// /// Default mapping of Polly s to objects. /// public static readonly IDictionary> DefaultErrorMapping = new Dictionary> { {typeof(TaskCanceledException), CreateRequestTimedOutError}, {typeof(TimeoutRejectedException), CreateRequestTimedOutError}, {typeof(BrokenCircuitException), CreateRequestTimedOutError}, {typeof(BrokenCircuitException), CreateRequestTimedOutError}, }; private static Error CreateRequestTimedOutError(Exception e) => new RequestTimedOutError(e); /// /// Adds Polly QoS provider to Ocelot by custom delegate and with custom error mapping. /// /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// Your customized delegating handler (to manage QoS behavior by yourself). /// Your customized error mapping. /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler, IDictionary> errorMapping) where TProvider : class, IPollyQoSResiliencePipelineProvider { builder.Services .AddSingleton>() .AddSingleton(errorMapping) // Dictionary injection used in HttpExceptionToErrorMapper .AddSingleton, TProvider>() .AddSingleton(delegatingHandler); return builder; } /// /// Adds Polly QoS provider to Ocelot with custom error mapping, but default is used. /// /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// Your customized error mapping. /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, IDictionary> errorMapping) where TProvider : class, IPollyQoSResiliencePipelineProvider => AddPolly(builder, GetDelegatingHandler, errorMapping); /// /// Adds Polly QoS provider to Ocelot with custom delegate, but default error mapping is used. /// /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// Your customized delegating handler (to manage QoS behavior by yourself). /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, QosDelegatingHandlerDelegate delegatingHandler) where TProvider : class, IPollyQoSResiliencePipelineProvider => AddPolly(builder, delegatingHandler, DefaultErrorMapping); /// /// Adds Polly QoS provider to Ocelot by defaults. /// /// /// Defaults: /// /// /// /// /// /// QoS provider to use (by default use ). /// Ocelot builder to extend. /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) where TProvider : class, IPollyQoSResiliencePipelineProvider => AddPolly(builder, GetDelegatingHandler, DefaultErrorMapping); /// /// Adds Polly QoS provider to Ocelot by defaults with default QoS provider. /// /// /// Defaults: /// /// /// /// /// /// /// Ocelot builder to extend. /// The reference to the same extended object. public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) => AddPolly(builder, GetDelegatingHandler, DefaultErrorMapping); /// /// Creates default delegating handler based on the type. /// /// The downstream route to apply the handler for. /// The context accessor of the route. /// The factory of logger. /// A object, but concrete type is the class. private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new PollyResiliencePipelineDelegatingHandler(route, contextAccessor, loggerFactory); } ================================================ FILE: src/Ocelot.Provider.Polly/OcelotResiliencePipelineKey.cs ================================================ using Polly.Registry; namespace Ocelot.Provider.Polly; /// /// Object used to identify a resilience pipeline in . /// /// The key for the resilience pipeline. public record OcelotResiliencePipelineKey(string Key); ================================================ FILE: src/Ocelot.Provider.Polly/PollyQoSResiliencePipelineProvider.cs ================================================ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; using Polly.CircuitBreaker; using Polly.Registry; using Polly.Timeout; using System.Linq; using System.Net; namespace Ocelot.Provider.Polly; /// /// Default provider for Polly V8 pipelines. /// public class PollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider { private readonly ResiliencePipelineRegistry _registry; private readonly IOcelotLogger _logger; public PollyQoSResiliencePipelineProvider( IOcelotLoggerFactory loggerFactory, ResiliencePipelineRegistry registry) { _logger = loggerFactory?.CreateLogger() ?? throw new ArgumentNullException(nameof(loggerFactory)); _registry = registry ?? throw new ArgumentNullException(nameof(registry)); } public static readonly IReadOnlySet DefaultServerErrorCodes = new HashSet() { HttpStatusCode.InternalServerError, HttpStatusCode.NotImplemented, HttpStatusCode.BadGateway, HttpStatusCode.ServiceUnavailable, HttpStatusCode.GatewayTimeout, HttpStatusCode.HttpVersionNotSupported, HttpStatusCode.VariantAlsoNegotiates, HttpStatusCode.InsufficientStorage, HttpStatusCode.LoopDetected, }; protected virtual HashSet ServerErrorCodes { get; } = DefaultServerErrorCodes as HashSet; protected virtual string GetRouteName(DownstreamRoute route) => route.Name(); /// /// Gets Polly V8 resilience pipeline (applies QoS feature) for the route. /// /// The downstream route to apply the pipeline for. /// A object where T is . public ResiliencePipeline GetResiliencePipeline(DownstreamRoute route) { ArgumentNullException.ThrowIfNull(route); if (!route.QosOptions.UseQos) { return ResiliencePipeline.Empty; // shortcut -> No QoS } return _registry.GetOrAddPipeline( key: new OcelotResiliencePipelineKey(route.LoadBalancerKey), configure: (builder) => ConfigureStrategies(builder, route)); } protected virtual void ConfigureStrategies(ResiliencePipelineBuilder builder, DownstreamRoute route) { ConfigureCircuitBreaker(builder, route); ConfigureTimeout(builder, route); } protected virtual string CircuitBreakerValidationMessage(DownstreamRoute route) => $"Route '{GetRouteName(route)}' has invalid {nameof(QoSOptions)} for Polly's Circuit Breaker strategy. Specifically, "; protected virtual bool IsConfigurationValidForCircuitBreaker(DownstreamRoute route) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(route.QosOptions); var qos = route.QosOptions; if (!qos.MinimumThroughput.HasValue || qos.MinimumThroughput <= 0) { _logger.LogError( () => CircuitBreakerValidationMessage(route) + $"the circuit breaker is disabled because the {nameof(qos.MinimumThroughput)} value ({ToStr(qos.MinimumThroughput)}) is either undefined, negative, or zero.", null); return false; } List> warnings = new(), w = warnings; if (!qos.MinimumThroughput.Value.IsValidMinimumThroughput()) { string msg1() => $"{The(w, msg1)} {nameof(CircuitBreakerStrategy.MinimumThroughput)} value ({qos.MinimumThroughput}) is less than the required {nameof(CircuitBreakerStrategy.LowMinimumThroughput)} threshold ({CircuitBreakerStrategy.LowMinimumThroughput}). Therefore, increase {nameof(qos.MinimumThroughput)} to at least {CircuitBreakerStrategy.LowMinimumThroughput} or higher. Until then, the default value ({CircuitBreakerStrategy.DefaultMinimumThroughput}) will be substituted."; warnings.Add(msg1); } if (qos.BreakDuration.HasValue && !qos.BreakDuration.Value.IsValidBreakDuration()) { string msg2() => $"{The(w, msg2)} {nameof(CircuitBreakerStrategy.BreakDuration)} value ({qos.BreakDuration}) is outside the valid range ({CircuitBreakerStrategy.LowBreakDuration} to {CircuitBreakerStrategy.HighBreakDuration} milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultBreakDuration}) will be substituted."; warnings.Add(msg2); } if (qos.FailureRatio.HasValue && !qos.FailureRatio.Value.IsValidFailureRatio()) { string msg3() => $"{The(w, msg3)} {nameof(CircuitBreakerStrategy.FailureRatio)} value ({qos.FailureRatio}) is outside the valid range ({CircuitBreakerStrategy.LowFailureRatio} to {CircuitBreakerStrategy.HighFailureRatio}). Therefore, ensure the ratio falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultFailureRatio}) will be substituted."; warnings.Add(msg3); } if (qos.SamplingDuration.HasValue && !qos.SamplingDuration.Value.IsValidSamplingDuration()) { string msg4() => $"{The(w, msg4)} {nameof(CircuitBreakerStrategy.SamplingDuration)} value ({qos.SamplingDuration}) is outside the valid range ({CircuitBreakerStrategy.LowSamplingDuration} to {CircuitBreakerStrategy.HighSamplingDuration} milliseconds). Therefore, ensure the duration falls within this range; otherwise, the default value ({CircuitBreakerStrategy.DefaultSamplingDuration}) will be substituted."; warnings.Add(msg4); } if (warnings.Count > 0) { _logger.LogWarning(() => CircuitBreakerValidationMessage(route) + string.Join(string.Empty, warnings.Select(f => f.Invoke()))); } return true; } protected virtual string TimeoutValidationMessage(DownstreamRoute route) => $"Route '{GetRouteName(route)}' has invalid {nameof(QoSOptions)} for Polly's Timeout strategy. Specifically, "; protected virtual bool IsConfigurationValidForTimeout(DownstreamRoute route) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(route.QosOptions); int? timeoutMs = route.QosOptions.Timeout; if (!timeoutMs.HasValue || timeoutMs.Value <= 0) { _logger.LogError( () => TimeoutValidationMessage(route) + $"the timeout is disabled because the {nameof(QoSOptions.Timeout)} ({ToStr(timeoutMs)}) is either undefined, negative, or zero.", null); return false; } List> warnings = new(), w = warnings; if (!timeoutMs.Value.IsValidTimeout()) { string msg() => $"{The(w, msg)} {nameof(TimeoutStrategy.Timeout)} value ({timeoutMs.Value}) is outside the valid range ({TimeoutStrategy.LowTimeout} to {TimeoutStrategy.HighTimeout} milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value ({TimeoutStrategy.DefaultTimeout}) will be substituted."; warnings.Add(msg); } if (warnings.Count > 0) { _logger.LogWarning(() => TimeoutValidationMessage(route) + string.Join(string.Empty, warnings.Select(f => f.Invoke()))); } return true; } public static string ToStr(int? value) => value.HasValue ? value.ToString() : "?"; public static string The(List> warnings, Func msg) => warnings.Count > 1 ? $"{Environment.NewLine} {warnings.IndexOf(msg) + 1}. The" : "the"; /// Configures the Circuit breaker resilience strategy. /// Pipeline builder instance. /// The route the pipeline is applied to. /// The same pipeline builder, as an object where TResult is . protected virtual ResiliencePipelineBuilder ConfigureCircuitBreaker(ResiliencePipelineBuilder builder, DownstreamRoute route) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(route.QosOptions); if (!IsConfigurationValidForCircuitBreaker(route)) { return builder; } var info = $"Circuit Breaker for the route: {GetRouteName(route)}: "; QoSOptions qos = route.QosOptions; int minimumThroughput = CircuitBreakerStrategy.MinimumThroughput(qos.MinimumThroughput ?? 0); // 0 fallbacks to the default value int breakDurationMs = CircuitBreakerStrategy.BreakDuration(qos.BreakDuration ?? 0); // 0 fallbacks to the default value double failureRatio = CircuitBreakerStrategy.FailureRatio(qos.FailureRatio ?? 0.0D); // 0 fallbacks to the default value int samplingDurationMs = CircuitBreakerStrategy.SamplingDuration(qos.SamplingDuration ?? 0); // 0 fallbacks to the default value var strategy = new CircuitBreakerStrategyOptions { FailureRatio = failureRatio, SamplingDuration = TimeSpan.FromMilliseconds(samplingDurationMs), MinimumThroughput = minimumThroughput, BreakDuration = TimeSpan.FromMilliseconds(breakDurationMs), ShouldHandle = new PredicateBuilder() .HandleResult(message => ServerErrorCodes.Contains(message.StatusCode)) .Handle() .Handle(), OnOpened = args => { _logger.LogError(info + $"Breaking for {args.BreakDuration.TotalMilliseconds} ms", args.Outcome.Exception); return ValueTask.CompletedTask; }, OnClosed = _ => { _logger.LogInformation(info + "Closed"); return ValueTask.CompletedTask; }, OnHalfOpened = _ => { // TODO: But the OnCircuitHalfOpened telemetry best practice recommends Warning 8-) // Read -> Circuit breaker Telemetry -> https://www.pollydocs.org/strategies/circuit-breaker.html#telemetry _logger.LogInformation(info + "Half Opened"); return ValueTask.CompletedTask; }, }; return builder.AddCircuitBreaker(strategy); } /// Configures the Timeout resilience strategy. /// Pipeline builder instance. /// The route the pipeline is applied to. /// The same pipeline builder, as an object where TResult is . protected virtual ResiliencePipelineBuilder ConfigureTimeout(ResiliencePipelineBuilder builder, DownstreamRoute route) { ArgumentNullException.ThrowIfNull(route); ArgumentNullException.ThrowIfNull(route.QosOptions); if (!IsConfigurationValidForTimeout(route)) { return builder; } int? timeoutMs = route.QosOptions.Timeout ?? TimeoutStrategy.DefaultTimeout; timeoutMs = TimeoutStrategy.Timeout(timeoutMs.Value) ?? TimeoutStrategy.DefaultTimeout; var strategy = new TimeoutStrategyOptions { Timeout = TimeSpan.FromMilliseconds(timeoutMs.Value), OnTimeout = _ => { _logger.LogInformation(() => $"Timeout for the route: {GetRouteName(route)}"); return ValueTask.CompletedTask; }, }; return builder.AddTimeout(strategy); } } ================================================ FILE: src/Ocelot.Provider.Polly/PollyResiliencePipelineDelegatingHandler.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; using Polly.CircuitBreaker; using System.Diagnostics; namespace Ocelot.Provider.Polly; public class PollyResiliencePipelineDelegatingHandler : DelegatingHandler { private readonly DownstreamRoute _route; private readonly IHttpContextAccessor _contextAccessor; private readonly IOcelotLogger _logger; public PollyResiliencePipelineDelegatingHandler( DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) { _route = route; _contextAccessor = contextAccessor; _logger = loggerFactory.CreateLogger(); } private IPollyQoSResiliencePipelineProvider GetQoSProvider() { Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); // TODO: Move IPollyQoSResiliencePipelineProvider object injection to DI container by a DI helper return _contextAccessor.HttpContext.RequestServices.GetService>(); } /// /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. /// /// Downstream request. /// Token to cancel the task. /// A object of a result. /// Exception thrown when a circuit is broken. /// Exception thrown by and classes. protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var provider = GetQoSProvider(); var pipeline = provider.GetResiliencePipeline(_route); if (pipeline == null) { #if DEBUG _logger.LogDebug(() => $"No pipeline was detected by QoS provider for the route with downstream URL '{request.RequestUri}'."); #endif return await base.SendAsync(request, cancellationToken); // shortcut > no qos } #if DEBUG _logger.LogInformation(() => $"The {pipeline} pipeline has detected by QoS provider for the route with downstream URL '{request.RequestUri}'. Going to execute request..."); #endif return await pipeline.ExecuteAsync(async (token) => await base.SendAsync(request, token), cancellationToken); } } ================================================ FILE: src/Ocelot.Provider.Polly/TimeoutStrategy.cs ================================================ using Ocelot.Configuration; namespace Ocelot.Provider.Polly; /// /// Polly requirements for the Timeout resilience strategy. /// The subject of this strategy is the property. /// public static class TimeoutStrategy { // Actual Polly's Timeout constraint -> https://www.pollydocs.org/api/Polly.Timeout.TimeoutStrategyOptions.html#Polly_Timeout_TimeoutStrategyOptions_Timeout public const int LowTimeout = 10; // 10 ms public const int DefTimeout = 30_000; // 30 seconds public const int HighTimeout = 86_400_000; // 24 hours in milliseconds /// /// Applies Polly's Timeout constraint to the value. /// If using Polly v8 or later, and in accordance with Polly's Timeout constraint, this value must be greater than (10 milliseconds) and less than (24 hours). /// The value in milliseconds. /// The same value if the constraint is satisfied; otherwise, . public static int? Timeout(int milliseconds) => IsValidTimeout(milliseconds) ? milliseconds : null; public static bool IsValidTimeout(this int milliseconds) => milliseconds > LowTimeout && milliseconds < HighTimeout; /// Gets or sets the default timeout in milliseconds, which overrides Polly's default of 30 seconds. /// The setter enforces Polly's Timeout constraint that the assigned value must fall within the range (, ). /// By default, initialized to (30 seconds). /// An value in milliseconds. public static int DefaultTimeout { get => defaultTimeout; set => defaultTimeout = Timeout(value) ?? DefTimeout; } private static int defaultTimeout = DefTimeout; } ================================================ FILE: src/Ocelot.Provider.Polly/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces // Project extra global namespaces global using Polly; global using System; global using System.Collections.Generic; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; ================================================ FILE: src/Ocelot.Provider.Polly/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net10.0/osx-x64": {}, "net10.0/win-x64": {}, "net8.0": { "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "8.0.2", "contentHash": "3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net8.0/osx-x64": {}, "net8.0/win-x64": {}, "net9.0": { "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "LezJ0enh6upO5EnPwACOZc/DdT1A8lvX6HPl/0rbe0eGt9rTDDPfx+Ny9OYZqf4g25Y3hOfWBQtRfMzueINNVQ==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } } }, "net9.0/osx-x64": {}, "net9.0/win-x64": {} } } ================================================ FILE: test/Ocelot.AcceptanceTests/.gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ build/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Microsoft Azure ApplicationInsights config file ApplicationInsights.config # Windows Store app package directory AppPackages/ BundleArtifacts/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe # FAKE - F# Make .fake/ ================================================ FILE: test/Ocelot.AcceptanceTests/Administration/AdministrationSteps.cs ================================================ using Ocelot.AcceptanceTests.Authentication; using Ocelot.Testing.Authentication; namespace Ocelot.AcceptanceTests.Administration; public sealed class AdministrationSteps : AuthenticationSteps { private Task GivenThereIsOcelotInternalJwtAuthServiceRunning(CancellationToken token) { var scopes = new string[] { OcelotScopes.Api, OcelotScopes.Api2 }; var jwtSigningServer = CreateJwtSigningServer(JwtSigningServerUrl, scopes); return jwtSigningServer.StartAsync(token) .ContinueWith(t => VerifyJwtSigningServerStarted(JwtSigningServerUrl, token), token); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Administration/CacheManagerTests.cs ================================================ //using Ocelot.Administration; //x using CacheManager.Core; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.AcceptanceTests.Authentication; //x using Ocelot.Cache.CacheManager; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Testing.Authentication; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Administration; public sealed class CacheManagerTests : AuthenticationSteps { public CacheManagerTests() : base() { } [Fact( DisplayName = "TODO " + nameof(ShouldClearCacheRegionViaAdministrationAPI), Skip = "TODO: Requires redevelopment after deprecation of Ocelot.Administration.IdentityServer4 package.")] public async Task ShouldClearCacheRegionViaAdministrationAPI() { //int port = PortFinder.GetRandomPort(); //var ocelotUrl = DownstreamUrl(port); var configuration = new FileConfiguration { Routes = [ GivenRoute(), GivenRoute("/test"), ], GlobalConfiguration = new() { //BaseUrl = ocelotUrl, }, }; //GivenThereIsAConfiguration(configuration); const string AdminPath = "/administration"; //GivenOcelotIsRunning(s => WithCacheManagerAndAdministrationForExternalJwtServer(s, AdminPath)); var port = await GivenOcelotHostIsRunning( WithBasicConfiguration, // Action configureDelegate, s => WithCacheManagerAndAdministrationForExternalJwtServer(s, AdminPath), // Action configureServices, WithUseOcelot, // Action configureApp, null, null, null, null); bool isExternal = true; await GivenThereIsExternalJwtSigningService([OcelotScopes.OcAdmin], Xunit.TestContext.Current.CancellationToken); var token = await GivenIHaveATokenWithUrlPath( path: !isExternal ? AdminPath : string.Empty, scope: OcelotScopes.OcAdmin); GivenIHaveAddedATokenToMyRequest(token); //await WhenIGetUrlOnTheApiGateway("/"); //ThenTheStatusCodeShouldBeOK(); // currently HttpStatusCode.BadGateway response = await ocelotClient.DeleteAsync($"{AdminPath}/outputcache/{TestName()}", Xunit.TestContext.Current.CancellationToken); ThenTheStatusCodeShouldBe(HttpStatusCode.NoContent); // currently HttpStatusCode.Unauthorized } public static FileCacheOptions DefaultFileCacheOptions { get; set; } = new() { TtlSeconds = 10, }; private FileRoute GivenRoute(string upstream = null, FileCacheOptions options = null) => new() { DownstreamHostAndPorts = [ Localhost(80) ], DownstreamScheme = Uri.UriSchemeHttps, DownstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], UpstreamPathTemplate = upstream ?? "/", FileCacheOptions = options ?? DefaultFileCacheOptions, }; private void WithCacheManagerAndAdministrationForExternalJwtServer(IServiceCollection services, string adminPath, [CallerMemberName] string testName = nameof(CacheManagerTests)) { //x static void WithSettings(ConfigurationBuilderCachePart settings) //x { //x settings.WithDictionaryHandle(); //x } services.AddMvc(option => option.EnableEndpointRouting = false); services.AddOcelot() //x.AddCacheManager(WithSettings) //.AddAdministration(adminPath, "secret") // this is for internal server .AddAdministration(adminPath, testName, externalJwtServer: new Uri(JwtSigningServerUrl)); // this is for external server } public override void Dispose() { Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE", string.Empty); Environment.SetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD", string.Empty); base.Dispose(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Administration/OcelotBuilderExtensions.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Ocelot.Administration; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; using Ocelot.Infrastructure.Extensions; using Ocelot.Middleware; using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Cryptography.X509Certificates; using System.Text; using System.Threading.Tasks; namespace Ocelot.AcceptanceTests.Administration; public static class OcelotBuilderExtensions { public static IOcelotBuilder AddAdministration(this IOcelotBuilder builder, string path, string apiSecret, Action configureOptions = null, Uri externalJwtServer = null) { var administrationPath = new AdministrationPath(path, apiSecret, externalJwtServer); builder.Services .AddSingleton(administrationPath) .AddSingleton(GetOcelotMiddlewareConfiguration); //var jwtServerConfiguration = GetIdentityServerConfiguration(secret); //AddIdentityServer(identityServerConfiguration, administrationPath, builder, builder.Configuration); var authBuilder = builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme); authBuilder = configureOptions is not null ? authBuilder.AddJwtBearer(configureOptions) : authBuilder.AddJwtBearer(); return builder; } public static Task GetOcelotMiddlewareConfiguration(IApplicationBuilder builder) { var repo = builder.ApplicationServices.GetService(); var config = repo.Get(); var administrationPath = config?.Data?.AdministrationPath; var administration = builder.ApplicationServices.GetService(); if (administration.ExternalJwtSigningUrl != null) { builder.UseOcelotJwtServer(administration.ExternalJwtSigningUrl); // UseIdentityServer(); } if (administrationPath.IsNotEmpty() && administration.Path.IsNotEmpty()) { builder.Map(administrationPath, AddOcelotAdministrationControllers); } return Task.CompletedTask; } public static void AddOcelotAdministrationControllers(this IApplicationBuilder builder) => builder .UseAuthentication() .UseRouting() .UseAuthorization() .UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); endpoints.MapControllers(); }); public static IApplicationBuilder UseOcelotJwtServer(this IApplicationBuilder app, Uri externalJwtSigningUrl, bool requireInstance = false) { ArgumentNullException.ThrowIfNull(app); //app.Properties[AuthenticationMiddlewareSetKey] = true; //return app.UseMiddleware(); return app; } public static IdentityServerConfiguration GetIdentityServerConfiguration(string secret) { var credentialsSigningCertificateLocation = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE"); var credentialsSigningCertificatePassword = Environment.GetEnvironmentVariable("OCELOT_CERTIFICATE_PASSWORD"); return new IdentityServerConfiguration( "admin", false, secret, new List { "admin", "openid", "offline_access" }, credentialsSigningCertificateLocation, credentialsSigningCertificatePassword ); } public static IOcelotBuilder AddAdministration(this IOcelotBuilder builder, string path, Action configureOptions) { //var administrationPath = new AdministrationPath(path); //builder.Services.AddSingleton(IdentityServerMiddlewareConfigurationProvider.Get); //if (configureOptions != null) //{ // AddIdentityServer(configureOptions, builder); //} //builder.Services.AddSingleton(administrationPath); return builder; } /* private static void AddIdentityServer(Action configOptions, IOcelotBuilder builder) { builder.Services .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer("Bearer", configOptions); } private static void AddIdentityServer(IIdentityServerConfiguration identityServerConfiguration, IAdministrationPath adminPath, IOcelotBuilder builder, IConfiguration configuration) { builder.Services.TryAddSingleton(identityServerConfiguration); var identityServerBuilder = builder.Services .AddIdentityServer(o => { o.IssuerUri = "Ocelot"; o.EmitStaticAudienceClaim = true; }) .AddInMemoryApiScopes(ApiScopes(identityServerConfiguration)) .AddInMemoryApiResources(Resources(identityServerConfiguration)) .AddInMemoryClients(Client(identityServerConfiguration)); var urlFinder = new BaseUrlFinder(configuration); var baseSchemeUrlAndPort = urlFinder.Find(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); builder.Services .AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer("Bearer", options => { options.Authority = baseSchemeUrlAndPort + adminPath.Path; options.RequireHttpsMetadata = identityServerConfiguration.RequireHttps; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, }; }); //todo - refactor naming.. if (string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificateLocation) || string.IsNullOrEmpty(identityServerConfiguration.CredentialsSigningCertificatePassword)) { identityServerBuilder.AddDeveloperSigningCredential(); } else { //todo - refactor so calls method? var cert = new X509Certificate2(identityServerConfiguration.CredentialsSigningCertificateLocation, identityServerConfiguration.CredentialsSigningCertificatePassword, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable); identityServerBuilder.AddSigningCredential(cert); } }*/ } public class IdentityServerConfiguration { public IdentityServerConfiguration( string apiName, bool requireHttps, string apiSecret, List allowedScopes, string credentialsSigningCertificateLocation, string credentialsSigningCertificatePassword) { ApiName = apiName; RequireHttps = requireHttps; ApiSecret = apiSecret; AllowedScopes = allowedScopes; CredentialsSigningCertificateLocation = credentialsSigningCertificateLocation; CredentialsSigningCertificatePassword = credentialsSigningCertificatePassword; } public string ApiName { get; } public bool RequireHttps { get; } public List AllowedScopes { get; } public string ApiSecret { get; } public string CredentialsSigningCertificateLocation { get; } public string CredentialsSigningCertificatePassword { get; } } ================================================ FILE: test/Ocelot.AcceptanceTests/AggregateTests.cs ================================================ //using IdentityServer4.AccessTokenValidation; //using IdentityServer4.Extensions; //using IdentityServer4.Models; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Authorization; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.AcceptanceTests.Authentication; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Multiplexer; using System.Text; namespace Ocelot.AcceptanceTests; public sealed class AggregateTests : Steps { private readonly string[] _downstreamPaths; public AggregateTests() { _downstreamPaths = new string[3]; } [Fact] [Trait("Issue", "597")] public void Should_fix_issue_597() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key1data/{userid}", UpstreamHttpMethod = ["Get"], DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port, }, }, Key = "key1", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key2data/{userid}", UpstreamHttpMethod = ["Get"], DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port, }, }, Key = "key2", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key3data/{userid}", UpstreamHttpMethod = ["Get"], DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port, }, }, Key = "key3", }, new FileRoute { DownstreamPathTemplate = "/api/values?MailId={userid}", UpstreamPathTemplate = "/key4data/{userid}", UpstreamHttpMethod = ["Get"], DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port, }, }, Key = "key4", }, }, Aggregates = new() { new FileAggregateRoute { RouteKeys = ["key1", "key2", "key3", "key4"], UpstreamPathTemplate = "/EmpDetail/IN/{userid}", }, new FileAggregateRoute { RouteKeys = ["key1", "key2"], UpstreamPathTemplate = "/EmpDetail/US/{userid}", }, }, GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "CorrelationID", }, }; var expected = "{\"key1\":some_data,\"key2\":some_data}"; this.Given(x => x.GivenServiceIsRunning(port, HttpStatusCode.OK, "some_data")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/EmpDetail/US/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void Should_return_response_200_with_advanced_aggregate_configs() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port1, }, }, UpstreamPathTemplate = "/Comments", UpstreamHttpMethod = ["Get"], Key = "Comments", }, new FileRoute { DownstreamPathTemplate = "/users/{userId}", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port2, }, }, UpstreamPathTemplate = "/UserDetails/{userId}", UpstreamHttpMethod = ["Get"], Key = "UserDetails", }, new FileRoute { DownstreamPathTemplate = "/posts/{postId}", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port3, }, }, UpstreamPathTemplate = "/PostDetails/{postId}", UpstreamHttpMethod = ["Get"], Key = "PostDetails", }, }, Aggregates = new() { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Comments", "UserDetails", "PostDetails"], RouteKeysConfig = new() { new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, }, }, }, }; var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}"; var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}"; var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, commentsResponseContent)) .Given(x => x.GivenServiceIsRunning(1, port2, "/users/1", 200, userDetailsResponseContent)) .Given(x => x.GivenServiceIsRunning(2, port3, "/posts/2", 200, postDetailsResponseContent)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_user_defined_aggregate() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port1, }, }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = ["Get"], Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port2, }, }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = ["Get"], Key = "Tom", }, }, Aggregates = new() { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Laura", "Tom"], Aggregator = "FakeDefinedAggregator", }, }, }; var expected = "Bye from Laura, Bye from Tom"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/laura", "Laura"); var route2 = GivenRoute(port2, "/tom", "Tom"); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}")) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_one_service_404() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port1, }, }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = ["Get"], Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port2, }, }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = ["Get"], Key = "Tom", }, }, Aggregates = new() { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Laura", "Tom"], }, }, }; var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, "")) .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_both_service_404() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port1, }, }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = ["Get"], Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port2, }, }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = ["Get"], Key = "Tom", }, }, Aggregates = new() { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Laura", "Tom"], }, }, }; var expected = "{\"Laura\":,\"Tom\":}"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, "")) .Given(x => x.GivenServiceIsRunning(1, port2, "/", 404, "")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } [Fact] public void Should_be_thread_safe() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port1, }, }, UpstreamPathTemplate = "/laura", UpstreamHttpMethod = ["Get"], Key = "Laura", }, new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new() { new FileHostAndPort { Host = "localhost", Port = port2, }, }, UpstreamPathTemplate = "/tom", UpstreamHttpMethod = ["Get"], Key = "Tom", }, }, Aggregates = new() { new FileAggregateRoute { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Laura", "Tom"], }, }, }; this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIMakeLotsOfDifferentRequestsToTheApiGateway()) .And(x => ThenTheDownstreamUrlPathShouldBe("/", "/")) .BDDfy(); } private void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() { var numberOfRequests = 100; var aggregateUrl = "/"; var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; var tomUrl = "/tom"; var tomExpected = "{Hello from Tom}"; var lauraUrl = "/laura"; var lauraExpected = "{Hello from Laura}"; var aggregateTasks = new Task[numberOfRequests]; for (var i = 0; i < numberOfRequests; i++) { aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); } var tomTasks = new Task[numberOfRequests]; for (var i = 0; i < numberOfRequests; i++) { tomTasks[i] = Fire(tomUrl, tomExpected, random); } var lauraTasks = new Task[numberOfRequests]; for (var i = 0; i < numberOfRequests; i++) { lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); } Task.WaitAll(lauraTasks); Task.WaitAll(tomTasks); Task.WaitAll(aggregateTasks); } private async Task Fire(string url, string expectedBody, Random random) { var request = new HttpRequestMessage(new HttpMethod("GET"), url); await Task.Delay(random.Next(0, 2)); var response = await ocelotClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); content.ShouldBe(expectedBody); } //[Fact] //[Trait("Bug", "1396")] //public void Should_return_response_200_with_user_forwarding() //{ // var port1 = PortFinder.GetRandomPort(); // var port2 = PortFinder.GetRandomPort(); // var port3 = PortFinder.GetRandomPort(); // var route1 = GivenRouteWithKey(port1, "/laura", "Laura"); // var route2 = GivenRouteWithKey(port2, "/tom", "Tom"); // var configuration = GivenConfiguration(route1, route2); // var identityServerUrl = $"{Uri.UriSchemeHttp}://localhost:{port3}"; // void configureOptions(IdentityServerAuthenticationOptions o) // { // o.Authority = identityServerUrl; // o.ApiName = "api"; // o.RequireHttpsMetadata = false; // o.SupportedTokens = SupportedTokens.Both; // o.ApiSecret = "secret"; // o.ForwardDefault = IdentityServerAuthenticationDefaults.AuthenticationScheme; // } // Action configureServices = s => // { // s.AddOcelot(); // s.AddMvcCore(mvc => // { // var policy = new AuthorizationPolicyBuilder() // .RequireAuthenticatedUser() // .RequireClaim("scope", "api") // .Build(); // mvc.Filters.Add(new AuthorizeFilter(policy)); // }); // s.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) // .AddIdentityServerAuthentication(configureOptions); // }; // var count = 0; // var actualContexts = new HttpContext[2]; // Action configureApp = async (app) => // { // var configuration = new OcelotPipelineConfiguration // { // PreErrorResponderMiddleware = async (context, next) => // { // var auth = await context.AuthenticateAsync(); // context.User = (auth.Succeeded && auth.Principal?.IsAuthenticated() == true) // ? auth.Principal : null; // await next.Invoke(); // }, // AuthorizationMiddleware = (context, next) => // { // actualContexts[count++] = context; // return next.Invoke(); // }, // }; // await app.UseOcelot(configuration); // }; // using (var auth = new AuthenticationTests()) // { // this.Given(x => auth.GivenThereIsAnIdentityServerOn(identityServerUrl, AccessTokenType.Jwt)) // .And(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}")) // .And(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}")) // .And(x => auth.GivenToken(identityServerUrl)) // .And(x => auth.GivenThereIsAConfiguration(configuration)) // .And(x => auth.GivenOcelotIsRunning(configureServices, configureApp)) // .And(x => auth.GivenIHaveAddedATokenToMyRequest()) // .When(x => auth.WhenIGetUrlOnTheApiGatewayWithRequestId("/")) // .Then(x => auth.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) // .And(x => auth.ThenTheResponseBodyShouldBe("{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}")) // .And(x => x.ThenTheDownstreamUrlPathShouldBe("/", "/")) // .BDDfy(); // } // // Assert // for (var i = 0; i < actualContexts.Length; i++) // { // var ctx = actualContexts[i].ShouldNotBeNull(); // ctx.Items.DownstreamRoute().Key.ShouldBe(configuration.Routes[i].Key); // var user = ctx.User.ShouldNotBeNull(); // user.IsAuthenticated().ShouldBeTrue(); // user.Claims.Count().ShouldBeGreaterThan(1); // user.Claims.FirstOrDefault(c => c is { Type: "scope", Value: "api" }).ShouldNotBeNull(); // } //} [Fact] [Trait("Bug", "2039")] public void Should_return_response_200_with_copied_body_sent_on_multiple_services() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); var configuration = GivenConfiguration(route1, route2); var requestBody = @"{""id"":1,""response"":""fromBody-#REPLACESTRING#""}"; var sub1ResponseContent = @"{""id"":1,""response"":""fromBody-s1""}"; var sub2ResponseContent = @"{""id"":1,""response"":""fromBody-s2""}"; var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s1"))) .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s2"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayWithBody("/", requestBody)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] [Trait("Bug", "2039")] public void Should_return_response_200_with_copied_form_sent_on_multiple_services() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/Service1", "Service1", "/Sub1"); var route2 = GivenRoute(port2, "/Service2", "Service2", "/Sub2"); var configuration = GivenConfiguration(route1, route2); var formValues = new[] { new KeyValuePair("param1", "value1"), new KeyValuePair("param2", "from-form-REPLACESTRING"), }; var sub1ResponseContent = "\"[key:param1=value1¶m2=from-form-s1]\""; var sub2ResponseContent = "\"[key:param1=value1¶m2=from-form-s2]\""; var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}"; this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqForm => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s1"))) .Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqForm => FormatFormCollection(reqForm).Replace("REPLACESTRING", "s2"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayWithForm("/", "key", formValues)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } private static string FormatFormCollection(IFormCollection reqForm) { var sb = new StringBuilder() .Append('"'); foreach (var kvp in reqForm) { sb.Append($"[{kvp.Key}:{kvp.Value}]"); } return sb .Append('"') .ToString(); } private void GivenServiceIsRunning(int port, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, context => { context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(responseBody); }); } private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody) => GivenServiceIsRunning(index, port, basePath, statusCode, async context => { await context.Response.WriteAsync(responseBody); }); private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromBody) => GivenServiceIsRunning(index, port, basePath, statusCode, async context => { var requestBody = await new StreamReader(context.Request.Body).ReadToEndAsync(); var responseBody = responseFromBody(requestBody); await context.Response.WriteAsync(responseBody); }); private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func responseFromForm) => GivenServiceIsRunning(index, port, basePath, statusCode, async context => { var responseBody = responseFromForm(context.Request.Form); await context.Response.WriteAsync(responseBody); }); private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action processContext) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { _downstreamPaths[index] = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (_downstreamPaths[index] != basePath) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync("downstream path doesn't match base path"); } else { context.Response.StatusCode = statusCode; processContext?.Invoke(context); } }); } private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() where TAggregator : class, IDefinedAggregator where TDependency : class { static void WithSpecificAggregators(IServiceCollection services) => services .AddSingleton() .AddOcelot() .AddSingletonDefinedAggregator(); GivenOcelotIsRunning(WithSpecificAggregators); } private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath) { _downstreamPaths[0].ShouldBe(expectedDownstreamPathOne); _downstreamPaths[1].ShouldBe(expectedDownstreamPath); } private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new() { DownstreamPathTemplate = downstream ?? "/", DownstreamScheme = Uri.UriSchemeHttp, DownstreamHostAndPorts = new() { new("localhost", port) }, UpstreamPathTemplate = upstream, UpstreamHttpMethod = [HttpMethods.Get], Key = key, }; public override FileConfiguration GivenConfiguration(params FileRoute[] routes) { var conf = base.GivenConfiguration(routes); conf.Aggregates.Add( new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = new(routes.Select(r => r.Key)), // [ "Laura", "Tom" ], } ); return conf; } } public class FakeDep { } public class FakeDefinedAggregator : IDefinedAggregator { public FakeDefinedAggregator(FakeDep dep) { } public async Task Aggregate(List responses) { var one = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); var two = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); var merge = $"{one}, {two}"; merge = merge.Replace("Hello", "Bye").Replace("{", "").Replace("}", ""); var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); return new DownstreamResponse(new StringContent(merge), HttpStatusCode.OK, headers, "some reason"); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Authentication/AuthenticationTests.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Testing.Authentication; namespace Ocelot.AcceptanceTests.Authentication; public sealed class AuthenticationTests : AuthenticationSteps { public AuthenticationTests() { } [Fact] public void Should_return_401_using_identity_server_access_token() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, method: HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty(), Xunit.TestContext.Current.CancellationToken)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .When(x => WhenIPostUrlOnTheApiGateway("/", "postContent")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)) .BDDfy(); } [Fact] public async Task Should_return_response_200_using_identity_server() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken); await GivenIHaveAToken(); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenTheResponseBodyShouldBe("Hello from Laura"); } [Fact] public async Task Should_return_response_401_using_identity_server_with_token_requested_for_other_api() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura"); GivenThereIsAConfiguration(configuration); static void WithOtherApiAudience(JwtBearerOptions o) { o.Audience = "other.api.com"; o.TokenValidationParameters.ValidAudience = "other.api.com"; } void WithOtherApiBearerAuthentication(IServiceCollection services) { services.AddOcelot(); Action configureOptions = WithThreemammalsOptions; services.AddAuthentication().AddJwtBearer(configureOptions + WithOtherApiAudience); } GivenOcelotIsRunning(WithOtherApiBearerAuthentication); await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken); var token = await GivenIHaveAToken(scope: "api2"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized); } [Fact] public async Task Should_return_201_using_identity_server_access_token() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, method: HttpMethods.Post); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken); await GivenIHaveAToken(); GivenIHaveAddedATokenToMyRequest(); await WhenIPostUrlOnTheApiGateway("/", "postContent"); ThenTheStatusCodeShouldBe(HttpStatusCode.Created); } [Theory] [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 [InlineData(true, HttpStatusCode.OK)] [InlineData(false, HttpStatusCode.Unauthorized)] public async Task Should_use_global_authentication(bool hasToken, HttpStatusCode status) { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); route.AuthenticationOptions.AuthenticationProviderKeys = []; // no route auth! var configuration = GivenConfiguration(route); configuration.GlobalConfiguration = GivenGlobalAuthConfiguration(); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK); await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken); if (hasToken) { await GivenIHaveAToken(); GivenIHaveAddedATokenToMyRequest(); } await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(status); ThenTheResponseBodyShouldBe(hasToken ? Body() : string.Empty); } [Fact] [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 public async Task Should_allow_anonymous_route_and_return_200_when_global_auth_options_and_no_token() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, allowAnonymous: true); route.AuthenticationOptions.AuthenticationProviderKeys = []; var configuration = GivenConfiguration(route); configuration.GlobalConfiguration = GivenGlobalAuthConfiguration(); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK); await GivenThereIsExternalJwtSigningService([], Xunit.TestContext.Current.CancellationToken); // await GivenIHaveAToken(); // GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBody(); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 public async Task ShouldApplyGlobalAuthenticationOptions_ForStaticRoutes() { var ports = PortFinder.GetPorts(3); var route1 = GivenAuthRoute(ports[0], "/route1", options: null); // no opts -> use global opts var route2 = GivenAuthRoute(ports[1], "/route2", GivenOptions(false, ["api"], ["test", JwtBearerDefaults.AuthenticationScheme])); var route3 = GivenAuthRoute(ports[2], "/noAuthorization", GivenOptions(false, ["invalid-scope"])); var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions = new(GivenOptions(false, ["apiGlobal"], [JwtBearerDefaults.AuthenticationScheme])); GivenThereIsAServiceRunningOnPath(ports[0], "/route1"); GivenThereIsAServiceRunningOnPath(ports[1], "/route2"); GivenThereIsAServiceRunningOnPath(ports[2], "/noAuthorization"); GivenThereIsAConfiguration(configuration); Action withAuth = WithJwtBearerAuthentication; void WithOAuthNotConfigured(IServiceCollection services) => services .AddAuthentication() .AddOAuth(route2.AuthenticationOptions.AuthenticationProviderKeys[0], opts => opts.ClientSecret = "bla-bla... actually, there are no options"); // -> 'test' scheme and it is registered now, but the auth will fail GivenOcelotIsRunning(withAuth + WithOAuthNotConfigured); await GivenThereIsExternalJwtSigningService(["api", "apiGlobal", "Mr.Who"], Xunit.TestContext.Current.CancellationToken); await GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/route1"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBody(); await GivenIHaveAToken(scope: route2.AuthenticationOptions.AllowedScopes[0]); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/route2"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBody(); await GivenIHaveAToken(scope: "Mr.Who"); // should be different scope of route #3 which is "invalid-scope" GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/noAuthorization"); ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden); await ThenTheResponseBodyShouldBeEmpty(); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 public async Task ShouldApplyGlobalGroupAuthenticationOptions_ForStaticRoutes_WhenRouteOptsHasAKey() { // 1st route var ports = PortFinder.GetPorts(3); var route1 = GivenAuthRoute(ports[0], "/route1", options: null); // no opts -> no auth at all route1.Key = null; // 1st route is not in the global group // 2nd route var route2 = GivenAuthRoute(ports[1], "/route2", options: null); // 2nd route opts will be applied from global ones route2.Key = "R2"; // 2nd route is in the group // 3rd route var route3 = GivenAuthRoute(ports[2], "/noAuthorization", GivenOptions(false, ["invalid-scope"], [JwtBearerDefaults.AuthenticationScheme])); var configuration = GivenConfiguration(route1, route2, route3); var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions = new(GivenOptions(false, ["apiGlobal"], [JwtBearerDefaults.AuthenticationScheme])) { RouteKeys = ["R2"], }; GivenThereIsAServiceRunningOnPath(ports[0], "/route1"); GivenThereIsAServiceRunningOnPath(ports[1], "/route2"); GivenThereIsAServiceRunningOnPath(ports[2], "/noAuthorization"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenThereIsExternalJwtSigningService(["api", "apiGlobal", "Mr.Who"], Xunit.TestContext.Current.CancellationToken); await GivenIHaveAToken(scope: "Mr.Who"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/route1"); ThenTheStatusCodeShouldBeOK(); // auth is switched off and the scope doesn't matter ThenTheResponseBody(); await GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/route2"); ThenTheStatusCodeShouldBeOK(); // global scope has been accepted ThenTheResponseBody(); await GivenIHaveAToken(scope: "Mr.Who"); // should be different scope of route #3 which is "invalid-scope" GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/noAuthorization"); ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden); await ThenTheResponseBodyShouldBeEmpty(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Authentication/MultipleAuthSchemesFeatureTests.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Ocelot.DependencyInjection; using Ocelot.Testing.Authentication; using System.Net.Http.Headers; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Authentication; [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 public sealed class MultipleAuthSchemesFeatureTests : AuthenticationSteps { private string[] _serverUrls; private BearerToken[] _tokens; public MultipleAuthSchemesFeatureTests() : base() { _serverUrls = Array.Empty(); _tokens = Array.Empty(); } private MultipleAuthSchemesFeatureTests Setup(int totalSchemes) { _serverUrls = new string[totalSchemes]; _tokens = new BearerToken[totalSchemes]; return this; } [Theory] [InlineData("Test1", "Test2")] // with multiple schemes [InlineData(JwtBearerDefaults.AuthenticationScheme, "Test")] // with default scheme [InlineData("Test", JwtBearerDefaults.AuthenticationScheme)] // with default scheme public async Task Should_authenticate_using_multiple_schemes(string scheme1, string scheme2) { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, scheme: "bla-bla"); //, validScope: "api2"); // TODO Need further dev string[] authSchemes = new[] { scheme1, scheme2 }; route.AuthenticationOptions.AuthenticationProviderKeys = authSchemes; var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port); GivenThereIsAConfiguration(configuration); Setup(authSchemes.Length); _serverUrls[0] = await GivenThereIsExternalJwtSigningService(["invalid", "unknown"], Xunit.TestContext.Current.CancellationToken); _serverUrls[1] = await GivenThereIsExternalJwtSigningService(["api1", "api2"], Xunit.TestContext.Current.CancellationToken); GivenOcelotIsRunningWithIdentityServerAuthSchemes("api2", authSchemes); await GivenIHaveTokenWithScope(0, "invalid"); // authentication should fail because of invalid scope await GivenIHaveTokenWithScope(1, "api2"); // authentication should succeed GivenIHaveAddedAllAuthHeaders(authSchemes); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe(Body()); } private async Task GivenIHaveTokenWithScope(int index, string scope, [CallerMemberName] string testName = "") { string url = _serverUrls[index]; _tokens[index] = await GivenIHaveAToken(scope, null, url, testName); } private void GivenIHaveAddedAllAuthHeaders(string[] schemes) { // Assume default scheme token is attached as "Authorization" header, for example "Bearer" // But default authentication setup should be ignored in multiple schemes scenario ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "failed"); for (int i = 0; i < schemes.Length && i < _tokens.Length; i++) { var token = _tokens[i]; var header = AuthHeaderName(schemes[i]); var hvalue = new AuthenticationHeaderValue(token.TokenType, token.AccessToken); GivenIAddAHeader(header, hvalue.ToString()); } } private static string AuthHeaderName(string scheme) => $"Oc-{HeaderNames.Authorization}-{scheme}"; private void WithBearerOptions(JwtBearerOptions o, string scheme, string issuerUrl) { AuthenticationTokenRequest request = AuthTokens[issuerUrl]; string authority = new Uri(JwtSigningServerUrl).Authority; o.Audience = request.Audience; o.Authority = authority; o.RequireHttpsMetadata = false; o.TokenValidationParameters = new() { ValidateIssuer = true, ValidIssuer = authority, ValidateAudience = true, ValidAudience = request.Audience, // ocelotClient.BaseAddress.Authority, ValidateIssuerSigningKey = true, IssuerSigningKey = request.IssuerSigningKey(), }; o.ForwardDefaultSelector = (context) => // TODO TokenRetriever ? { var headers = context.Request.Headers; var name = AuthHeaderName(scheme); if (headers.TryGetValue(name, out StringValues value)) { // Redirect to default authentication handler which is (JwtAuthHandler) aka (Bearer) headers[HeaderNames.Authorization] = value; return scheme; } // Something wrong with the setup: no headers, no tokens. // Redirect to default scheme to read token from default header return JwtBearerDefaults.AuthenticationScheme; }; } private void GivenOcelotIsRunningWithIdentityServerAuthSchemes(string validScope, params string[] schemes) { GivenOcelotIsRunning(services => { var ocelot = services.AddOcelot(); var auth = services.AddAuthentication(o => { o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }); for (int i = 0; i < schemes.Length; i++) { var scheme = schemes[i]; var issuerUrl = _serverUrls[i]; auth.AddJwtBearer(scheme, o => WithBearerOptions(o, scheme, issuerUrl)); } }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Authorization/AuthorizationSteps.cs ================================================ using Ocelot.Testing.Authentication; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Authorization; public class AuthorizationSteps : AuthenticationSteps { public void GivenIUpdateSubClaim() => AuthTokenRequesting += UpdateSubClaim; protected virtual void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e) { var uid = e.Request.UserId; e.Request.UserId = string.Concat(OcelotScopes.OcAdmin, "|", uid); // -> sub claim -> oc-sub claim } public const string DefaultAudience = null; public Task GivenIHaveATokenWithScope(string scope, [CallerMemberName] string testName = "") => GivenIHaveAToken(scope, null, JwtSigningServerUrl, DefaultAudience, testName); public Task GivenIHaveATokenWithClaims(IEnumerable> claims, [CallerMemberName] string testName = "") => GivenIHaveAToken(OcelotScopes.Api, claims, JwtSigningServerUrl, DefaultAudience, testName); } ================================================ FILE: test/Ocelot.AcceptanceTests/Authorization/AuthorizationTests.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Testing.Authentication; using System.Security.Claims; namespace Ocelot.AcceptanceTests.Authorization; public sealed class AuthorizationTests : AuthorizationSteps { private static Dictionary GivenRouteClaimsRequirement(FileRoute route, string claimType, string claimValue) { route.AddHeadersToRequest = new() { { "CustomerId", "Claims[CustomerId] > value" }, { "LocationId", "Claims[LocationId] > value" }, { "UserType", $"Claims[{OcelotClaims.OcSub}] > value[0] > |" }, { "UserId", $"Claims[{OcelotClaims.OcSub}] > value[1] > |" }, }; route.AddClaimsToRequest = new() { { "CustomerId", "Claims[CustomerId] > value" }, { "UserType", $"Claims[{OcelotClaims.OcSub}] > value[0] > |" }, { "UserId", $"Claims[{OcelotClaims.OcSub}] > value[1] > |" }, }; var claims = new Dictionary() { {"CustomerId", "111"}, {"LocationId", "222"}, {"UserType", "registered"}, }; route.RouteClaimsRequirement = new(claims) // require all custom claims { [claimType] = claimValue, // but require exact claim with the scope after claims-to-claims transformation }; return claims; } [Fact] [Trait("Commit", "3285be3")] // https://github.com/ThreeMammals/Ocelot/commit/3285be3 [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public void Should_return_200_OK_authorizing_route() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); var configuration = GivenConfiguration(route); var claims = GivenRouteClaimsRequirement(route, "UserType", OcelotScopes.OcAdmin); var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty(), Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Commit", "b8951c4")] // https://github.com/ThreeMammals/Ocelot/commit/b8951c4 [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public void Should_return_403_Forbidden_authorizing_route() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); var configuration = GivenConfiguration(route); var claims = GivenRouteClaimsRequirement(route, "UserType", OcelotScopes.OcAdmin); route.AddClaimsToRequest.Remove("UserType"); // given I don't transform UserType claim var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty(), Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) .And(x => ThenTheResponseBodyShouldBeEmpty()) .BDDfy(); } [Fact] [Trait("Feat", "100")] // https://github.com/ThreeMammals/Ocelot/issues/100 [Trait("PR", "104")] // https://github.com/ThreeMammals/Ocelot/pull/104 [Trait("Release", "1.4.5")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5 public async Task Should_return_200_OK_using_identity_server_with_allowed_scope() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["api", "api.readOnly", "openid", "offline_access"]; var route = GivenAuthRoute(port, scopes: allowedScopes); var configuration = GivenConfiguration(route); await GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenIHaveAToken(scope: "api.readOnly"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); await ThenTheResponseBodyShouldBeAsync("Hello from Laura"); } [Fact] [Trait("Feat", "100")] // https://github.com/ThreeMammals/Ocelot/issues/100 [Trait("PR", "104")] // https://github.com/ThreeMammals/Ocelot/pull/104 [Trait("Release", "1.4.5")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5 public void Should_return_403_Forbidden_using_identity_server_with_scope_not_allowed() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["api", "openid", "offline_access"]; var route = GivenAuthRoute(port, scopes: allowedScopes); var configuration = GivenConfiguration(route); var testName = TestName(); var allScopes = allowedScopes.Append("api.readOnly").ToArray(); this.Given(x => GivenThereIsExternalJwtSigningService(allScopes, Xunit.TestContext.Current.CancellationToken)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => GivenIHaveATokenWithScope("api.readOnly", testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden)) .BDDfy(); } /// /// In ASP.NET Core, the (used for appsettings.json and similar) does not behave like a plain . /// It applies normalization rules to keys when loading configuration. /// That's why keys starting with "http://" or "https://" don't deserialize as you expect. /// /// AI search: /// C# ASP.NET JsonConfigurationProvider Keys with "http://" prefix are not deserialized into dictionary. [Fact(DisplayName = "TODO " + nameof(Should_fix_issue_240))] [Trait("Bug", "240")] // https://github.com/ThreeMammals/Ocelot/issues/240 [Trait("PR", "243")] // https://github.com/ThreeMammals/Ocelot/pull/243 [Trait("Release", "3.1.6")] // https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.6 public void Should_fix_issue_240() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); var configuration = GivenConfiguration(route); route.RouteClaimsRequirement = new() // TODO this is dictionary which doesn't support multiple keys of the same value { { ClaimTypes.Role, "User"}, // TODO Such a claim types in a form of URL (aka http://*) are not supported by JsonConfigurationProvider { nameof(ClaimTypes.Role), "User"}, // this key is Ok because it is not an URL containing proto delimiter aka '://' }; var claims = new List>() { new(nameof(ClaimTypes.Role), "AdminUser"), new(nameof(ClaimTypes.Role), "User"), }; var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(Array.Empty(), Xunit.TestContext.Current.CancellationToken)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Release", "24.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 public async Task Should_return_200_OK_with_global_allowed_scopes() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port); route.AuthenticationOptions.AuthenticationProviderKeys = []; // no route auth! var configuration = GivenConfiguration(route); string[] globalScopes = ["api", "apiGlobal"]; configuration.GlobalConfiguration = GivenGlobalAuthConfiguration(allowedScopes: globalScopes); GivenThereIsAConfiguration(configuration); await GivenThereIsExternalJwtSigningService(globalScopes, Xunit.TestContext.Current.CancellationToken); GivenThereIsAServiceRunningOn(port); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenIHaveAToken(scope: "apiGlobal"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); await ThenTheResponseBodyAsync(); } #region PR 1478 [Fact] [Trait("Bug", "913")] // https://github.com/ThreeMammals/Ocelot/issues/913 [Trait("PR", "1478")] // https://github.com/ThreeMammals/Ocelot/pull/1478 [Trait("Release", "24.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.1.0 public async Task Should_return_200_OK_with_space_separated_scope_match() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, scopes: ["api", "api.read", "api.write"]); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); await GivenThereIsExternalJwtSigningService(["api.read", "openid", "offline_access"], Xunit.TestContext.Current.CancellationToken); GivenThereIsAServiceRunningOn(port); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenIHaveATokenWithScope("api.read openid offline_access"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); await ThenTheResponseBodyAsync(); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] [Trait("Release", "24.1.0")] public async Task Should_return_403_Forbidden_with_space_separated_scope_no_match() { var port = PortFinder.GetRandomPort(); var route = GivenAuthRoute(port, scopes: ["admin", "superuser"]); var configuration = GivenConfiguration(route); await GivenThereIsExternalJwtSigningService(["api.read", "api.write", "openid"], Xunit.TestContext.Current.CancellationToken); GivenThereIsAServiceRunningOn(port); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithJwtBearerAuthentication); await GivenIHaveATokenWithScope("api.read api.write openid"); GivenIHaveAddedATokenToMyRequest(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.Forbidden); } #endregion PR 1478 } ================================================ FILE: test/Ocelot.AcceptanceTests/Caching/CachingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using System.Text; using JsonSerializer = System.Text.Json.JsonSerializer; namespace Ocelot.AcceptanceTests.Caching; public sealed class CachingTests : Steps { private const string HelloTomContent = "Hello from Tom"; private const string HelloLauraContent = "Hello from Laura"; private int _counter = 0; public CachingTests() { } [Fact] public void Should_return_cached_response() { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 100, }; var configuration = GivenFileConfiguration(port, options); this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, null, null)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .And(x => ThenTheContentLengthIs(HelloLauraContent.Length)) .BDDfy(); } [Fact] public void Should_return_cached_response_with_expires_header() { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 100, }; var configuration = GivenFileConfiguration(port, options); var headerExpires = "Expires"; this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, headerExpires, "-1")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .And(x => ThenTheContentLengthIs(HelloLauraContent.Length)) .And(x => ThenTheResponseContentHeaderIs(headerExpires, "-1")) .BDDfy(); } [Fact] public void Should_not_return_cached_response_as_ttl_expires() { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 1, }; var configuration = GivenFileConfiguration(port, options); this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, null, null)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, null, null)) .And(x => GivenTheCacheExpires()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloTomContent)) .BDDfy(); } [Theory] [InlineData(true)] [InlineData(false)] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void Should_return_different_cached_response_when_request_body_changes_and_EnableContentHashing_is_true(bool asGlobalConfig) { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 100, EnableContentHashing = true, }; var (testBody1String, testBody2String) = TestBodiesFactory(); var configuration = GivenFileConfiguration(port, options, asGlobalConfig, HttpMethods.Post); this.Given(x => x.GivenThereIsAnEchoServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody2String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody2String)) .And(x => ThenTheCounterValueShouldBe(2)) .BDDfy(); } [Theory] [InlineData(true)] [InlineData(false)] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void Should_return_same_cached_response_when_request_body_changes_and_EnableContentHashing_is_false(bool asGlobalConfig) { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 100, }; var (testBody1String, testBody2String) = TestBodiesFactory(); var configuration = GivenFileConfiguration(port, options, asGlobalConfig, HttpMethods.Post); this.Given(x => x.GivenThereIsAnEchoServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody1String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(testBody2String, Encoding.UTF8, "application/json"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(testBody1String)) .And(x => ThenTheCounterValueShouldBe(1)) .BDDfy(); } [Fact] [Trait("Issue", "1172")] public void Should_clean_cached_response_by_cache_header_via_new_caching_key() { var port = PortFinder.GetRandomPort(); var options = new FileCacheOptions { TtlSeconds = 100, Region = "europe-central", Header = "Authorization", }; var configuration = GivenFileConfiguration(port, options); var headerExpires = "Expires"; // Add to cache this.Given(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, HelloLauraContent, headerExpires, options.TtlSeconds)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) // Read from cache .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, headerExpires, options.TtlSeconds / 2)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloLauraContent)) .And(x => ThenTheContentLengthIs(HelloLauraContent.Length)) // Clean cache by the header and cache new content .Given(x => x.GivenTheServiceNowReturns(port, HttpStatusCode.OK, HelloTomContent, headerExpires, -1)) .And(x => GivenIAddAHeader(options.Header, "123")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(HelloTomContent)) .And(x => ThenTheContentLengthIs(HelloTomContent.Length)) .BDDfy(); } private FileConfiguration GivenFileConfiguration(int port, FileCacheOptions cacheOptions, bool asGlobalConfig = false, params string[] methods) { var route = GivenRoute(port); route.CacheOptions = asGlobalConfig ? new() { TtlSeconds = cacheOptions.TtlSeconds } : cacheOptions; foreach (var m in methods) route.UpstreamHttpMethod.Add(m); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration = !asGlobalConfig ? null : new() { CacheOptions = new(cacheOptions), }; return configuration; } private static void GivenTheCacheExpires() { Thread.Sleep(1000); } private void GivenTheServiceNowReturns(int port, HttpStatusCode statusCode, string responseBody, string key, object value) { handler.Dispose(); GivenThereIsAServiceRunningOn(port, statusCode, responseBody, key, value); } private void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, string responseBody, string key, object value) { handler.GivenThereIsAServiceRunningOn(port, context => { if (!string.IsNullOrEmpty(key) && value != null) { context.Response.Headers.Append(key, value.ToString()); } context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(responseBody); }); } [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1013:Public method should be marked as test", Justification = "Steps")] [System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "Steps")] public void GivenThereIsAnEchoServiceRunningOn(int port) { handler.GivenThereIsAServiceRunningOn(port, async context => { using var streamReader = new StreamReader(context.Request.Body); var requestBody = await streamReader.ReadToEndAsync(); _counter++; context.Response.StatusCode = (int)HttpStatusCode.OK; await context.Response.WriteAsync(requestBody); }); } private void ThenTheCounterValueShouldBe(int expected) { Assert.Equal(expected, _counter); } public static (string TestBody1String, string TestBody2String) TestBodiesFactory() { var testBody1 = new TestBody { Age = 19, Email = "tom@ocelot.net", FirstName = "Tom", LastName = "Test", }; var testBody1String = JsonSerializer.Serialize(testBody1); var testBody2 = new TestBody { Age = 25, Email = "laura@ocelot.net", FirstName = "Laura", LastName = "Test", }; var testBody2String = JsonSerializer.Serialize(testBody2); return (testBody1String, testBody2String); } } public class TestBody { public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public int Age { get; set; } } ================================================ FILE: test/Ocelot.AcceptanceTests/CancelRequestTests.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.AcceptanceTests; public sealed class CancelRequestTests : Steps, IDisposable { public CancelRequestTests() { } [Fact] public async Task ShouldAbortServiceWork_WhenCancellingTheRequest() { // Arrange var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); var started = new Notifier("service work started notifier"); var stopped = new Notifier("service work finished notifier"); GivenThereIsAServiceRunningOn(DownstreamUrl(port), started, stopped); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); // Act: Initialize var getting = WhenIGetUrl("/"); var canceling = WhenIWaitForNotification(started).ContinueWith(Cancel); Exception ex = null; // Act try { await Task.WhenAll(getting, canceling); } catch (Exception e) { ex = e; } // Assert started.NotificationSent.ShouldBeTrue(); stopped.NotificationSent.ShouldBeFalse(); ex.ShouldNotBeNull().ShouldBeOfType(); } private Task Cancel(Task t) => Task.Run(ocelotClient.CancelPendingRequests); private void GivenThereIsAServiceRunningOn(string baseUrl, Notifier startedNotifier, Notifier stoppedNotifier) { handler.GivenThereIsAServiceRunningOn(baseUrl, async context => { startedNotifier.NotificationSent = true; await Task.Delay(SERVICE_WORK_TIME, context.RequestAborted); context.Response.StatusCode = (int)HttpStatusCode.OK; await context.Response.WriteAsync("OK"); stoppedNotifier.NotificationSent = true; }); } private const int SERVICE_WORK_TIME = 1_000; private const int WAITING_TIME = 50; private const int MAX_WAITING_TIME = 10_000; private static async Task WhenIWaitForNotification(Notifier notifier) { int waitingTime = 0; while (!notifier.NotificationSent) { await Task.Delay(WAITING_TIME); waitingTime += WAITING_TIME; if (waitingTime > MAX_WAITING_TIME) { throw new TimeoutException(notifier.Name + $" did not sent notification within {MAX_WAITING_TIME / 1000} second(s)."); } } } class Notifier { public Notifier(string name) => Name = name; public bool NotificationSent { get; set; } public string Name { get; set; } } } ================================================ FILE: test/Ocelot.AcceptanceTests/CannotStartOcelotTests.cs ================================================ namespace Ocelot.AcceptanceTests; public class CannotStartOcelotTests : Steps { private static readonly string NL = Environment.NewLine; [Fact] public void Should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered_with_dynamic_re_routes() { var invalidConfig = GivenConfiguration(); invalidConfig.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = "https", Host = "localhost", Type = nameof(Provider.Consul.Consul), Port = 8500, }; Exception exception = null; GivenThereIsAConfiguration(invalidConfig); try { GivenOcelotIsRunning(); } catch (Exception ex) { exception = ex; } exception.ShouldNotBeNull(); exception.Message.ShouldBe($"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?{NL}"); } [Fact] public void Should_throw_exception_if_cannot_start_because_service_discovery_provider_specified_in_config_but_no_service_discovery_provider_registered() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/laura", "/"); var invalidConfig = GivenConfiguration(route); invalidConfig.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = "https", Host = "localhost", Type = nameof(Provider.Consul.Consul), Port = 8500, }; Exception exception = null; GivenThereIsAConfiguration(invalidConfig); try { GivenOcelotIsRunning(); } catch (Exception ex) { exception = ex; } exception.ShouldNotBeNull(); exception.Message.ShouldBe($"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?{NL}"); } [Fact] public void Should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_globally() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/laura", "/"); var invalidConfig = GivenConfiguration(route); invalidConfig.GlobalConfiguration.QoSOptions = new() { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; Exception exception = null; GivenThereIsAConfiguration(invalidConfig); try { GivenOcelotIsRunning(); } catch (Exception ex) { exception = ex; } exception.ShouldNotBeNull(); exception.Message.ShouldBe($"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?{NL}"); } [Fact] public void Should_throw_exception_if_cannot_start_because_no_qos_delegate_registered_for_re_route() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/laura", "/"); route.QoSOptions = new() { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; var invalidConfig = GivenConfiguration(route); Exception exception = null; GivenThereIsAConfiguration(invalidConfig); try { GivenOcelotIsRunning(); } catch (Exception ex) { exception = ex; } exception.ShouldNotBeNull(); exception.Message.ShouldBe($"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?{NL}"); } [Fact] public void Should_throw_exception_if_cannot_start() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "api", "test"); var invalidConfig = GivenConfiguration(route); Exception exception = null; GivenThereIsAConfiguration(invalidConfig); try { GivenOcelotIsRunning(); } catch (Exception ex) { exception = ex; } exception.ShouldNotBeNull(); exception.Message.ShouldBe($"Unable to start Ocelot, errors are:{NL}FileValidationFailedError: Downstream Path Template test doesnt start with forward slash{NL}FileValidationFailedError: Upstream Path Template api doesnt start with forward slash{NL}"); } } ================================================ FILE: test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests; public sealed class CaseSensitiveRoutingTests : Steps { public CaseSensitiveRoutingTests() { } [Fact] public void Should_return_response_200_when_global_ignore_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = ["Get"], }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_200_when_route_ignore_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = false, }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_404_when_route_respect_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_return_response_200_when_route_respect_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_404_when_global_respect_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/products/{productId}", UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_return_response_200_when_global_respect_case_sensitivity_set() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/products/{productId}", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/PRODUCTS/{productId}", UpstreamHttpMethod = ["Get"], RouteIsCaseSensitive = true, }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", 200, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/PRODUCTS/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ConcurrentSteps.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.AcceptanceTests.LoadBalancer; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer; using System.Collections.Concurrent; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; namespace Ocelot.AcceptanceTests; public class ConcurrentSteps : Steps { protected Task[] _tasks; protected ConcurrentDictionary _responses; protected volatile int[] _counters; public ConcurrentSteps() { _tasks = Array.Empty(); _responses = new(); _counters = Array.Empty(); } public override void Dispose() { foreach (var response in _responses.Values) { response?.Dispose(); } foreach (var task in _tasks) { task?.Dispose(); } base.Dispose(); GC.SuppressFinalize(this); } protected void GivenServiceInstanceIsRunning(string url, string response) => GivenServiceInstanceIsRunning(url, response, HttpStatusCode.OK); protected void GivenServiceInstanceIsRunning(string url, string response, HttpStatusCode statusCode) { _counters = new int[1]; // single counter GivenServiceIsRunning(url, response, 0, statusCode); _counters[0] = 0; } protected void GivenThereIsAServiceRunningOn(string url, string basePath, string responseBody) { handler.GivenThereIsAServiceRunningOn(url, basePath, MapGet(basePath, responseBody)); } protected void GivenMultipleServiceInstancesAreRunning(string[] urls, [CallerMemberName] string serviceName = null) { serviceName ??= new Uri(urls[0]).Host; string[] responses = urls.Select(u => $"{serviceName}|url({u})").ToArray(); GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK); } protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses) => GivenMultipleServiceInstancesAreRunning(urls, responses, HttpStatusCode.OK); protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses, HttpStatusCode statusCode) { Debug.Assert(urls.Length == responses.Length, "Length mismatch!"); _counters = new int[urls.Length]; // multiple counters for (int i = 0; i < urls.Length; i++) { GivenServiceIsRunning(urls[i], responses[i], i, statusCode); _counters[i] = 0; } } protected void GivenMultipleServiceInstancesAreRunning(string[] urls, string[] responses, HttpStatusCode[] codes) { Debug.Assert(urls.Length == responses.Length, "Length mismatch!"); Debug.Assert(urls.Length == codes.Length, "Length mismatch!"); Debug.Assert(responses.Length == codes.Length, "Length mismatch!"); _counters = new int[urls.Length]; // multiple counters for (int i = 0; i < urls.Length; i++) { GivenServiceIsRunning(urls[i], responses[i], i, codes[i]); _counters[i] = 0; } } private void GivenServiceIsRunning(string url, string response) => GivenServiceIsRunning(url, response, 0, HttpStatusCode.OK); private void GivenServiceIsRunning(string url, string response, int index) => GivenServiceIsRunning(url, response, index, HttpStatusCode.OK); private void GivenServiceIsRunning(string url, string response, int index, HttpStatusCode successCode) { response ??= successCode.ToString(); handler.GivenThereIsAServiceRunningOn(url, MapGet(index, response, successCode)); } protected static RequestDelegate MapGet(string path, string responseBody) => MapGet(path, responseBody, HttpStatusCode.OK); protected static RequestDelegate MapGet(string path, string responseBody, HttpStatusCode statusCode) => async context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool isMatch = downstreamPath == path; context.Response.StatusCode = (int)(isMatch ? statusCode : HttpStatusCode.NotFound); await context.Response.WriteAsync(isMatch ? responseBody : "Not Found"); }; public static class HeaderNames { public const string ServiceIndex = nameof(LeaseEventArgs.ServiceIndex); public const string Host = nameof(Uri.Host); public const string Port = nameof(Uri.Port); public const string Counter = nameof(Counter); public const string Path = nameof(Path); } protected RequestDelegate MapGet(int index, string body) => MapGet(index, body, HttpStatusCode.OK); protected RequestDelegate MapGet(int index, string body, HttpStatusCode successCode) => async context => { // Don't delay during the first service call if (Volatile.Read(ref _counters[index]) > 0) { await Task.Delay(Random.Shared.Next(5, 15)); // emulate integration delay up to 15 milliseconds } string responseBody; var request = context.Request; var response = context.Response; try { int count = Interlocked.Increment(ref _counters[index]); responseBody = string.Concat(count, CounterSeparator, body); response.StatusCode = (int)successCode; response.Headers.Append(HeaderNames.ServiceIndex, new StringValues(index.ToString())); response.Headers.Append(HeaderNames.Host, new StringValues(request.Host.Host)); response.Headers.Append(HeaderNames.Port, new StringValues(request.Host.Port.ToString())); response.Headers.Append(HeaderNames.Counter, new StringValues(count.ToString())); response.Headers.Append(HeaderNames.Path, new StringValues(request.Path + request.QueryString)); await response.WriteAsync(responseBody); } catch (Exception exception) { responseBody = string.Concat(1, CounterSeparator, exception.StackTrace); response.StatusCode = (int)HttpStatusCode.InternalServerError; await response.WriteAsync(responseBody); } }; public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(string url, int times) => RunParallelRequests(times, (i) => url); public Task[] WhenIGetUrlOnTheApiGatewayConcurrently(int times, params string[] urls) => RunParallelRequests(times, (i) => urls[i % urls.Length]); protected Task[] RunParallelRequests(int times, Func urlFunc) { _tasks = new Task[times]; _responses = new(times, times); for (var i = 0; i < times; i++) { var url = urlFunc(i); _tasks[i] = GetParallelResponse(url, i); _responses[i] = null; } Task.WaitAll(_tasks); return _tasks; } protected const string CounterSeparator = "^:^"; private async Task GetParallelResponse(string url, int threadIndex) { var response = await ocelotClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); var counterString = content.Contains(CounterSeparator) ? content.Split(CounterSeparator)[0] // let the first fragment is counter value : "0"; int count = int.Parse(counterString); if (content.IsNotEmpty()) count.ShouldBeGreaterThan(0); _responses[threadIndex] = response; } public void ThenAllStatusCodesShouldBe(HttpStatusCode expected) => _responses.ShouldAllBe(response => response.Value.StatusCode == expected); public void ThenAllResponseBodiesShouldBe(string expectedBody) { foreach (var r in _responses) { var content = r.Value.Content.ReadAsStringAsync().Result; content = content?.Contains(CounterSeparator) == true ? content.Split(CounterSeparator)[1] // remove counter for body comparison : "0"; content.ShouldBe(expectedBody); } } public void ThenAllResponseBodiesShouldBe(int[] ports, string[] expected) { foreach (var r in _responses) { var response = r.Value; var portHeader = response.Headers.GetValues("Port").Csv(); int port = int.Parse(portHeader); int i = Array.IndexOf(ports, port); var expectedBody = expected[i]; var content = response.Content.ReadAsStringAsync().Result; content = content?.Contains(CounterSeparator) == true ? content.Split(CounterSeparator)[1] // remove counter for body comparison : "0"; content.ShouldBe(expectedBody); } } protected string CalledTimesMessage() => $"All values are [{_counters.Csv()}]"; public void ThenAllServicesShouldHaveBeenCalledTimes(int expected) => _counters.Sum().ShouldBe(expected, CalledTimesMessage()); public void ThenServiceShouldHaveBeenCalledTimes(int index, int expected) => _counters[index].ShouldBe(expected, CalledTimesMessage()); public void ThenServicesShouldHaveBeenCalledTimes(params int[] expected) { for (int i = 0; i < expected.Length; i++) { _counters[i].ShouldBe(expected[i], CalledTimesMessage()); } } public static int Bottom(int totalRequests, int totalServices) => totalRequests / totalServices; public static int Top(int totalRequests, int totalServices) { int bottom = Bottom(totalRequests, totalServices); return totalRequests - (bottom * totalServices) + bottom; } public void ThenAllServicesCalledRealisticAmountOfTimes(int bottom, int top) { var customMessage = new StringBuilder() .AppendLine($"{nameof(bottom)}: {bottom}") .AppendLine($" {nameof(top)}: {top}") .AppendLine($" All values are [{_counters.Csv()}]") .ToString(); int sum = 0, totalSum = _counters.Sum(); // Last offline services cannot be called at all, thus don't assert zero counters for (int i = 0; i < _counters.Length && sum < totalSum; i++) { int actual = _counters[i]; actual.ShouldBeInRange(bottom, top, customMessage); sum += actual; } } public void ThenAllServicesCalledOptimisticAmountOfTimes(ILoadBalancerAnalyzer analyzer) { if (analyzer == null) return; int bottom = analyzer.BottomOfConnections(), top = analyzer.TopOfConnections(); bottom = Math.Min(bottom, _counters.Min()); ThenAllServicesCalledRealisticAmountOfTimes(bottom, top); // with unstable checkings } public void ThenServiceCountersShouldMatchLeasingCounters(ILoadBalancerAnalyzer analyzer, int[] ports, int totalRequests) { if (analyzer == null || ports == null) return; analyzer.ShouldNotBeNull().Analyze(); analyzer.Events.Count.ShouldBe(totalRequests, $"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}"); var leasingCounters = analyzer?.GetHostCounters() ?? new(); var sortedLeasingCountersByPort = ports.Select(port => leasingCounters.FirstOrDefault(kv => kv.Key.DownstreamPort == port).Value).ToArray(); for (int i = 0; i < ports.Length; i++) { var host = leasingCounters.Keys.FirstOrDefault(k => k.DownstreamPort == ports[i]); // Leasing info/counters can be absent because of offline service instance with exact port in unstable scenario if (host != null) { var customMessage = new StringBuilder() .AppendLine($"{nameof(ILoadBalancerAnalyzer.ServiceName)}: {analyzer.ServiceName}") .AppendLine($" Port: {ports[i]}") .AppendLine($" Host: {host}") .AppendLine($" Service counters: [{_counters.Csv()}]") .AppendLine($" Leasing counters: [{sortedLeasingCountersByPort.Csv()}]") // should have order of _counters .ToString(); int counter1 = _counters[i]; int counter2 = leasingCounters[host]; counter1.ShouldBe(counter2, customMessage); } } } protected IEnumerable ThenAllResponsesHeaderExists(string key) { foreach (var kv in _responses) { var response = kv.Value.ShouldNotBeNull(); response.Headers.Contains(key).ShouldBeTrue(); var header = response.Headers.GetValues(key); yield return string.Join(';', header); } } protected virtual string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? GetType().Name; protected virtual string ServiceNamespace() => GetType().Namespace; } ================================================ FILE: test/Ocelot.AcceptanceTests/Configuration/ConfigurationInConsulTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; using System.Text; namespace Ocelot.AcceptanceTests.Configuration; public sealed class ConfigurationInConsulTests : Steps { private FileConfiguration _config; private readonly List _consulServices; public ConfigurationInConsulTests() { _consulServices = new List(); } [Fact] public void Should_return_response_200_with_simple_url_when_using_jsonserialized_cache() { var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(servicePort); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = Uri.UriSchemeHttp, Host = "localhost", Port = consulPort, }; this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty)) .And(x => GivenThereIsAServiceRunningOn(servicePort, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } private void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() { static void WithConsulToStoreConfigAndJsonSerializedCache(IServiceCollection services) => services .AddOcelot() .AddConsul() .AddConfigStoredInConsul(); GivenOcelotIsRunning(WithConsulToStoreConfigAndJsonSerializedCache); } private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int consulPort, string serviceName) { handler.GivenThereIsAServiceRunningOn(consulPort, async context => { if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.InvariantCultureIgnoreCase) && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { var json = JsonConvert.SerializeObject(_config); var bytes = Encoding.UTF8.GetBytes(json); var base64 = Convert.ToBase64String(bytes); var kvp = new FakeConsulGetResponse(base64); // await context.Response.WriteJsonAsync(new[] { kvp }); } else if (context.Request.Method.Equals(HttpMethods.Put, StringComparison.InvariantCultureIgnoreCase) && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { try { var reader = new StreamReader(context.Request.Body); // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. // var json = reader.ReadToEnd(); var json = await reader.ReadToEndAsync(); _config = JsonConvert.DeserializeObject(json); var response = JsonConvert.SerializeObject(true); await context.Response.WriteAsync(response); } catch (Exception e) { Console.WriteLine(e); throw; } } else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { //await context.Response.WriteJsonAsync(_consulServices); } }); } public class FakeConsulGetResponse { public FakeConsulGetResponse(string value) => Value = value; public int CreateIndex => 100; public int ModifyIndex => 200; public int LockIndex => 200; public string Key => "InternalConfiguration"; public int Flags => 0; public string Value { get; } public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; } } ================================================ FILE: test/Ocelot.AcceptanceTests/Configuration/ConfigurationMergeTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Configuration; public sealed class ConfigurationMergeTests : Steps { private readonly FileConfiguration _initialGlobalConfig; private readonly string _globalConfigFileName; public ConfigurationMergeTests() : base() { _initialGlobalConfig = new(); _globalConfigFileName = $"{TestID}-{ConfigurationBuilderExtensions.GlobalConfigFile}"; Files.Add(_globalConfigFileName); } [Theory] [Trait("Bug", "1216")] [Trait("Feat", "1227")] [InlineData(MergeOcelotJson.ToFile, true)] [InlineData(MergeOcelotJson.ToMemory, false)] public void ShouldRunWithGlobalConfigMerged_WithExplicitGlobalConfigFileParameter(MergeOcelotJson where, bool fileExist) { Arrange(); // Act GivenOcelotIsRunning((context, config) => config .SetBasePath(context.HostingEnvironment.ContentRootPath) .AddOcelot(_initialGlobalConfig, context.HostingEnvironment, where, ocelotConfigFileName, _globalConfigFileName, null, false, false)); // Assert TheOcelotPrimaryConfigFileExists(fileExist); ThenGlobalConfigurationHasBeenMerged(); } [Theory] [Trait("Bug", "2084")] [InlineData(MergeOcelotJson.ToFile, true)] [InlineData(MergeOcelotJson.ToMemory, false)] public void ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter(MergeOcelotJson where, bool fileExist) { Arrange(); var globalConfig = _initialGlobalConfig; globalConfig.Routes.Clear(); var routeAConfig = GivenConfiguration(GetRoute("A")); var routeBConfig = GivenConfiguration(GetRoute("B")); var environmentConfig = GivenConfiguration(GetRoute("Env")); environmentConfig.GlobalConfiguration = null; var folder = "GatewayConfiguration-" + TestID; Folders.Add(Directory.CreateDirectory(folder).FullName); var globalPath = Path.Combine(folder, ConfigurationBuilderExtensions.GlobalConfigFile); var routeAPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, "A")); var routeBPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, "B")); var environmentPath = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, "Env")); GivenThereIsAConfiguration(globalConfig, globalPath); GivenThereIsAConfiguration(routeAConfig, routeAPath); GivenThereIsAConfiguration(routeBConfig, routeBPath); GivenThereIsAConfiguration(environmentConfig, environmentPath); // Act GivenOcelotIsRunning( (context, config) => config .SetBasePath(context.HostingEnvironment.ContentRootPath) .AddOcelot(folder, context.HostingEnvironment, where) // overloaded version from the user's scenario .AddJsonFile(environmentPath), null, null, null, host => host.UseEnvironment("Env"), null, null); // Assert TheOcelotPrimaryConfigFileExists(false); ThenGlobalConfigurationHasBeenMerged(); var actualLocation = Path.Combine(folder, ConfigurationBuilderExtensions.PrimaryConfigFile); File.Exists(actualLocation).ShouldBe(fileExist); var repository = OcelotServices.GetService().ShouldNotBeNull(); var response = repository.Get().ShouldNotBeNull(); response.IsError.ShouldBeFalse(); var internalConfig = response.Data.ShouldNotBeNull(); // Assert Arrange() setup internalConfig.RequestId.ShouldBe(nameof(ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter)); internalConfig.ServiceProviderConfiguration.ConfigurationKey.ShouldBe(nameof(ShouldRunWithGlobalConfigMerged_WithImplicitGlobalConfigFileParameter)); } private void Arrange([CallerMemberName] string testName = null) { _initialGlobalConfig.GlobalConfiguration.RequestIdKey = testName; _initialGlobalConfig.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = testName; } private void TheOcelotPrimaryConfigFileExists(bool expected) => File.Exists(ocelotConfigFileName).ShouldBe(expected); private void ThenGlobalConfigurationHasBeenMerged([CallerMemberName] string testName = null) { var config = OcelotServices.GetService().ShouldNotBeNull(); var actual = config["GlobalConfiguration:RequestIdKey"]; actual.ShouldNotBeNull().ShouldBe(testName); actual = config["GlobalConfiguration:ServiceDiscoveryProvider:ConfigurationKey"]; actual.ShouldNotBeNull().ShouldBe(testName); } private static FileRoute GetRoute(string suffix, [CallerMemberName] string testName = null) => new() { DownstreamScheme = nameof(FileRoute.DownstreamScheme) + suffix, DownstreamPathTemplate = "/" + suffix, Key = testName + suffix, UpstreamPathTemplate = "/" + suffix, UpstreamHttpMethod = [ nameof(FileRoute.UpstreamHttpMethod) + suffix ], DownstreamHostAndPorts = new() { new(nameof(FileHostAndPort.Host) + suffix, 80), }, }; } ================================================ FILE: test/Ocelot.AcceptanceTests/Configuration/ConfigurationReloadTests.cs ================================================ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; namespace Ocelot.AcceptanceTests.Configuration; public sealed class ConfigurationReloadTests : Steps { private readonly FileConfiguration _initialConfig; private readonly FileConfiguration _anotherConfig; private IOcelotConfigurationChangeTokenSource _changeToken; public ConfigurationReloadTests() { _initialConfig = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "initialKey", }, }; _anotherConfig = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { RequestIdKey = "someOtherKey", }, }; } [Fact] public void Should_reload_config_on_change() { this.Given(x => GivenThereIsAConfiguration(_initialConfig)) .And(x => GivenOcelotIsRunningReloadingConfig(true)) .And(x => GivenThereIsAConfiguration(_anotherConfig)) .And(x => ThenConfigShouldBeWithTimeout(_anotherConfig, 10000)) .BDDfy(); } private async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) { var result = await Wait.For(timeoutMs).UntilAsync(async () => { var internalConfigCreator = OcelotServices.GetService(); var internalConfigRepo = OcelotServices.GetService(); var internalConfig = internalConfigRepo.Get(); var config = await internalConfigCreator.Create(fileConfig); return internalConfig.Data.RequestId == config.Data.RequestId; }); result.ShouldBe(true); } [Fact] public void Should_not_reload_config_on_change() { this.Given(x => GivenThereIsAConfiguration(_initialConfig)) .And(x => GivenOcelotIsRunningReloadingConfig(false)) .And(x => GivenThereIsAConfiguration(_anotherConfig)) .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken)) .And(x => ThenConfigShouldBe(_initialConfig)) .BDDfy(); } private async Task ThenConfigShouldBe(FileConfiguration fileConfig) { var internalConfigCreator = OcelotServices.GetService(); var internalConfigRepo = OcelotServices.GetService(); var internalConfig = internalConfigRepo.Get(); var config = await internalConfigCreator.Create(fileConfig); internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); } [Fact] public void Should_trigger_change_token_on_change() { this.Given(x => GivenThereIsAConfiguration(_initialConfig)) .And(x => GivenOcelotIsRunningReloadingConfig(true)) .And(x => GivenIHaveAChangeToken()) .And(x => GivenThereIsAConfiguration(_anotherConfig)) .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken)) .Then(x => TheChangeTokenShouldBeActive(true)) .BDDfy(); } [Fact] public void Should_not_trigger_change_token_with_no_change() { this.Given(x => GivenThereIsAConfiguration(_initialConfig)) .And(x => GivenOcelotIsRunningReloadingConfig(false)) .And(x => GivenIHaveAChangeToken()) .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken)) // Wait for prior activation to expire. .And(x => GivenThereIsAConfiguration(_anotherConfig)) .And(x => Steps.GivenIWait(MillisecondsToWaitForChangeToken)) .Then(x => TheChangeTokenShouldBeActive(false)) .BDDfy(); } private const int MillisecondsToWaitForChangeToken = (int)(OcelotConfigurationChangeToken.PollingIntervalSeconds * 1000) - 100; private void GivenOcelotIsRunningReloadingConfig(bool shouldReload) { GivenOcelotIsRunning((context, config) => config .SetBasePath(context.HostingEnvironment.ContentRootPath) .AddOcelot(ocelotConfigFileName, false, shouldReload)); } private void GivenIHaveAChangeToken() { _changeToken = OcelotServices.GetRequiredService(); } private void TheChangeTokenShouldBeActive(bool itShouldBeActive) { _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Configuration/DownstreamHttpVersionTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Ocelot.Configuration.File; using System.Security.Authentication; namespace Ocelot.AcceptanceTests.Configuration; [Trait("PR", "1127")] [Trait("Feat", "1124")] public sealed class DownstreamHttpVersionTests : Steps { [Fact] public void Should_return_response_200_when_using_http_one() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version10); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpProtocols.Http1)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_200_when_using_http_one_point_one() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version11); route.DangerousAcceptAnyServerCertificateValidator = true; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpProtocols.Http1)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_200_when_using_http_two_point_zero() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, Uri.UriSchemeHttps, HttpVersion.Version20); route.DangerousAcceptAnyServerCertificateValidator = true; var configuration = GivenConfiguration(route); const string expected = "here is some content"; var httpContent = new StringContent(expected); this.Given(x => x.GivenThereIsAServiceUsingHttpsRunningOn(port, "/", HttpProtocols.Http2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/", httpContent)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void Should_return_response_502_when_using_http_one_to_talk_to_server_running_http_two() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, Uri.UriSchemeHttps, HttpVersion.Version11); route.DangerousAcceptAnyServerCertificateValidator = true; var configuration = GivenConfiguration(route); const string expected = "here is some content"; var httpContent = new StringContent(expected); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpProtocols.Http2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/", httpContent)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } //TODO: does this test make any sense? [Fact] public void Should_return_response_200_when_using_http_two_to_talk_to_server_running_http_one_point_one() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, Uri.UriSchemeHttp, HttpVersion.Version11); route.DangerousAcceptAnyServerCertificateValidator = true; var configuration = GivenConfiguration(route); const string expected = "here is some content"; var httpContent = new StringContent(expected); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpProtocols.Http1)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/", httpContent)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } private FileRoute GivenRoute(int port, string scheme, Version httpVersion) => new() { DownstreamPathTemplate = "/{url}", DownstreamScheme = scheme ?? Uri.UriSchemeHttp, UpstreamPathTemplate = "/{url}", UpstreamHttpMethod = [HttpMethods.Get], DownstreamHostAndPorts = [Localhost(port)], DownstreamHttpMethod = HttpMethods.Get, DownstreamHttpVersion = httpVersion.ToString(), }; private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpProtocols protocols) { void options(KestrelServerOptions serverOptions) { serverOptions.Listen(IPAddress.Loopback, port, listenOptions => { listenOptions.Protocols = protocols; }); } handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, options, async context => { context.Response.StatusCode = (int)HttpStatusCode.OK; var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); await context.Response.WriteAsync(body); }); } private void GivenThereIsAServiceUsingHttpsRunningOn(int port, string basePath, HttpProtocols protocols) { void options(KestrelServerOptions serverOptions) { serverOptions.Listen(IPAddress.Loopback, port, listenOptions => { listenOptions.UseHttps("mycert2.pfx", "password", options => { options.SslProtocols = SslProtocols.Tls12; }); listenOptions.Protocols = protocols; }); } handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, options, async context => { context.Response.StatusCode = 200; var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); await context.Response.WriteAsync(body); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Configuration/TimeoutTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.File; using System.Diagnostics; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Configuration; [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 public class TimeoutTests : TimeoutTestsBase { [Fact] [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 public async Task HasRouteAndGlobalTimeouts_RouteTimeoutShouldTakePrecedenceOverGlobalTimeout() { const int RouteTimeoutSeconds = 2, GlobalTimeoutSeconds = 4; int serviceTimeoutMs = Ms(Math.Max(RouteTimeoutSeconds, GlobalTimeoutSeconds)) + 500; // total 4.5 sec var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port, RouteTimeoutSeconds, GlobalTimeoutSeconds); // !!! GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); var watcher = await WatchWhenIGetUrlOnTheApiGateway(); ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), Ms(RouteTimeoutSeconds) + 500); // (2.0, 2.5) s ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler await ThenTheResponseBodyShouldBeAsync(string.Empty); } [Fact] [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 public async Task HasGlobalTimeoutOnly_ForAllRoutesGlobalTimeoutShouldTakePrecedenceOverAbsoluteGlobalTimeout() { const int GlobalTimeoutSeconds = 2; int serviceTimeoutMs = Ms(GlobalTimeoutSeconds + 1); // total 3 sec var ports = PortFinder.GetPorts(2); FileRoute route1 = GivenRoute(ports[0], "/route1"), route2 = GivenRoute(ports[1], "/route2"); // without timeouts var configuration = GivenConfiguration(route1, route2); configuration.GlobalConfiguration.Timeout = GlobalTimeoutSeconds; // !!! GivenThereIsAServiceRunningOn(ports[0], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable GivenThereIsAServiceRunningOn(ports[1], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); var watchers = await Task.WhenAll( WatchWhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate), WatchWhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate)); int globalTimeoutMs = Ms(GlobalTimeoutSeconds); foreach (var watcher in watchers) { ThenTimeoutIsInRange(watcher, globalTimeoutMs, Ms(DownstreamRoute.DefaultTimeoutSeconds)); // (2.0, 90) so assert roughly ThenTimeoutIsInRange(watcher, globalTimeoutMs, globalTimeoutMs + 500); // (2.0, 2.5) so assert precisely ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler await ThenTheResponseBodyShouldBeAsync(string.Empty); } } [Fact] [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 public async Task HasRouteTimeout_ShouldTimeoutAfterRouteTimeout() { const int RouteTimeoutSeconds = 2; int serviceTimeoutMs = Ms(RouteTimeoutSeconds) + 500; // total 2.5 sec var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port, RouteTimeoutSeconds); // !!! GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 2.5s > 2s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); var watcher = await WatchWhenIGetUrlOnTheApiGateway(); ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), serviceTimeoutMs); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler await ThenTheResponseBodyShouldBeAsync(string.Empty); } [Collection(nameof(SequentialTests))] public class Sequential : TimeoutTestsBase { [Fact] [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 public async Task NoRouteTimeoutAndNoGlobalOne_ShouldTimeoutAfterCustomDefaultTimeout() { try { DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // override original 90s with 3s int serviceTimeoutMs = Ms(DownstreamRoute.LowTimeout) + 500; // total 3.5 sec var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port, routeTimeout: null, globalTimeout: null); // !!! no route timeout -> DownstreamRoute.DefaultTimeoutSeconds GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, serviceTimeoutMs); // 3.5s > 3s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); var watcher = await WatchWhenIGetUrlOnTheApiGateway(); ThenTimeoutIsInRange(watcher, Ms(DownstreamRoute.LowTimeout), serviceTimeoutMs); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 3 secs -> TimeoutException by TimeoutDelegatingHandler await ThenTheResponseBodyShouldBeAsync(string.Empty); } finally { DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; } } } } public class TimeoutTestsBase : Steps { protected static int Ms(int seconds) => 1000 * seconds; protected FileConfiguration GivenConfiguration(int port, int? routeTimeout = null, int? globalTimeout = null) { var route = GivenDefaultRoute(port); route.Timeout = routeTimeout; var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.Timeout = globalTimeout; return configuration; } protected virtual void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, int timeout, [CallerMemberName] string response = nameof(TimeoutTests)) { async Task MapBodyWithTimeout(HttpContext context) { await Task.Delay(timeout); context.Response.StatusCode = (int)statusCode; await context.Response.WriteAsync(response); } handler.GivenThereIsAServiceRunningOn(port, MapBodyWithTimeout); } protected async Task WatchWhenIGetUrlOnTheApiGateway(string upstream = null) { var watcher = Stopwatch.StartNew(); await WhenIGetUrlOnTheApiGateway(upstream ?? "/"); watcher.Stop(); return watcher; } protected static void ThenTimeoutIsInRange(Stopwatch watcher, int lowDurationMs, int highDurationMs) { var expectedLowDuration = TimeSpan.FromMilliseconds(lowDurationMs); var expectedHighDuration = TimeSpan.FromMilliseconds(highDurationMs); watcher.Elapsed.ShouldBeGreaterThan(expectedLowDuration); watcher.Elapsed.ShouldBeLessThan(expectedHighDuration); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ContentTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using System.Diagnostics; namespace Ocelot.AcceptanceTests; public sealed class ContentTests : Steps { private string _contentType; private long? _contentLength; private long _memoryUsageAfterCallToService; private bool _contentTypeHeaderExists; public ContentTests() : base() { } [Fact] public void Should_Not_add_content_type_or_content_length_headers() { var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheContentTypeShouldBeEmpty()) .And(x => ThenTheContentLengthShouldBeZero()) .BDDfy(); } [Fact] public void Should_add_content_type_and_content_length_headers() { var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port, HttpMethods.Post); var contentType = "application/json"; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.Created, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", "postContent", contentType)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) .And(x => ThenTheContentTypeIsIs(contentType)) .BDDfy(); } [Fact] public void Should_add_default_content_type_header() { var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port, HttpMethods.Post); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.Created, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", "postContent")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) .And(x => ThenTheContentTypeIsIs("text/plain; charset=utf-8")) .BDDfy(); } [Fact] [Trait("PR", "1824")] [Trait("Issues", "356 695 1924")] public void Should_Not_increase_memory_usage_When_downloading_large_file() { var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration(port); var dummyDatFilePath = GenerateDummyDatFile(100); this.Given(x => x.GivenThereIsAServiceWithPayloadRunningOn(port, "/", dummyDatFilePath)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => x.ThenMemoryUsageShouldNotIncrease()) .BDDfy(); } private void ThenMemoryUsageShouldNotIncrease() { var currentMemoryUsage = Process.GetCurrentProcess().WorkingSet64; var tolerance = currentMemoryUsage - (10 * 1024 * 1024L); Assert.InRange(_memoryUsageAfterCallToService, currentMemoryUsage - tolerance, currentMemoryUsage + tolerance); } private void ThenTheContentTypeIsIs(string expected) { _contentType.ShouldBe(expected); } private void ThenTheContentLengthShouldBeZero() { _contentLength.ShouldBeNull(); } private void ThenTheContentTypeShouldBeEmpty() { _contentType.ShouldBeNullOrEmpty(); _contentTypeHeaderExists.ShouldBe(false); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { _contentType = context.Request.ContentType; _contentLength = context.Request.ContentLength; _contentTypeHeaderExists = context.Request.Headers.TryGetValue("Content-Type", out var value); context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(responseBody); }); } private void GivenThereIsAServiceWithPayloadRunningOn(int port, string basePath, string dummyDatFilePath) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { context.Response.StatusCode = (int)HttpStatusCode.OK; await using var fileStream = File.OpenRead(dummyDatFilePath); await fileStream.CopyToAsync(context.Response.Body); _memoryUsageAfterCallToService = Process.GetCurrentProcess().WorkingSet64; }); } /// /// Generates a dummy payload of the given size in MB. /// Avoiding maintaining a large file in the repository. /// /// The file size in MB. /// The payload file path. /// Throwing an exception if the payload path is null. private static string GenerateDummyDatFile(int sizeInMb) { var payloadName = "dummy.dat"; var payloadPath = Path.Combine(Directory.GetCurrentDirectory(), payloadName); if (File.Exists(payloadPath)) { File.Delete(payloadPath); } var newFile = new FileStream(payloadPath, FileMode.CreateNew); try { newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin); newFile.WriteByte(0); } finally { newFile.Dispose(); } return payloadPath; } private static FileConfiguration GivenConfiguration(int port, string method = null) => new() { Routes = new() { new FileRoute { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, DownstreamHostAndPorts = new() { new FileHostAndPort("localhost", port), }, UpstreamPathTemplate = "/", UpstreamHttpMethod = [method ?? HttpMethods.Get], }, }, }; } ================================================ FILE: test/Ocelot.AcceptanceTests/Core/LoadTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Balancers; using System.Diagnostics; namespace Ocelot.AcceptanceTests.Core; /// /// TODO Move to separate Performance Testing (load testing) project. /// [Collection(nameof(SequentialTests))] public sealed class LoadTests : ConcurrentSteps { private string _downstreamPath; private string _downstreamQuery; /// /// This test should be moved to a separate project. It should be run during release only as an extra check for quality gates. /// /// A representing the asynchronous operation. [Fact] [Trait("PR", "1348")] // https://github.com/ThreeMammals/Ocelot/pull/1348 [Trait("Bug", "2246")] // https://github.com/ThreeMammals/Ocelot/issues/2246 public async Task ShouldLoadRegexCachingHeavily_NoMemoryLeaks() { Assert.SkipWhen(IsCiCd(), "Skipped in CI/CD! It should be moved to a separate project. It should be run during release only as an extra check for quality gates."); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/my-gateway/order/{orderNumber}", "/order/{orderNumber}"); route.LoadBalancerOptions = new(nameof(NoLoadBalancer)); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); GivenThereIsAServiceRunningOn(port, "/order/", "Hello from Donny"); // Step 1: Measure memory consumption for constant upstream URL GC.Collect(); var a = GC.GetGCMemoryInfo(); var totalMemory = ToMegabytes(GC.GetTotalMemory(true)); var currentProcess = Process.GetCurrentProcess(); var memoryUsage = ToMegabytes(currentProcess.WorkingSet64); // Perform 50% of job for stable indicators await WhenIDoActionMultipleTimes(5_000, (i) => WhenIGetUrlOnTheApiGateway("/my-gateway/order/1")); // const url GC.Collect(); var totalMemory0 = ToMegabytes(GC.GetTotalMemory(true)); var process0 = Process.GetCurrentProcess(); var memoryUsage0 = ToMegabytes(process0.WorkingSet64); await WhenIDoActionMultipleTimes(5_000, (i) => WhenIGetUrlOnTheApiGateway("/my-gateway/order/1")); // const url await WhenIGetUrlOnTheApiGateway("/my-gateway/order/1"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenTheResponseBodyShouldBe("Hello from Donny"); GC.Collect(); var totalMemory1 = ToMegabytes(GC.GetTotalMemory(true)); var process1 = Process.GetCurrentProcess(); var memoryUsage1 = ToMegabytes(process1.WorkingSet64); // Step 2: Measure memory consumption for varying upstream URL // await WhenIDoActionForTime(TimeSpan.FromSeconds(30), (i) => WhenIGetUrlOnTheApiGatewayWithRequestId("/my-gateway/order/" + i)); // varying url await WhenIDoActionMultipleTimes(10_000, (i) => WhenIGetUrlOnTheApiGateway("/my-gateway/order/" + i)); // varying url GC.Collect(); var totalMemory2 = ToMegabytes(GC.GetTotalMemory(true)); var process2 = Process.GetCurrentProcess(); var memoryUsage2 = ToMegabytes(process2.WorkingSet64); // Assert AssertDelta(totalMemory0, totalMemory1, totalMemory2); AssertDelta(memoryUsage0, memoryUsage1, memoryUsage2); } private static float ToMegabytes(long total) => (float)total / (1024 * 1024); private static void AssertDelta(float value0, float value1, float value2) { if (value1 <= value0) return; var delta = value1 - value0; if (value2 <= value1) return; var delta2 = value2 - value1; Assert.True(delta2 <= delta); // delta is not growing } private void GivenThereIsAServiceRunningOn(int port, string basePath, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, MapGet); Task MapGet(HttpContext context) { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value + context.Request.Path.Value : context.Request.Path.Value; _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty; var isOK = _downstreamPath.StartsWith(basePath); context.Response.StatusCode = isOK ? (int)HttpStatusCode.OK : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(isOK ? responseBody : nameof(HttpStatusCode.NotFound)); } } } ================================================ FILE: test/Ocelot.AcceptanceTests/Core/ThreadSafeHeadersTests.cs ================================================ using System.Collections.Concurrent; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.Core; // Old integration tests public sealed class ThreadSafeHeadersTests : Steps { private readonly ConcurrentBag _results; public ThreadSafeHeadersTests() { _results = new(); } [Fact] public void Should_return_same_response_for_each_different_header_under_load_to_downsteam_service() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK); GivenOcelotIsRunning(); WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues("/", 300); ThenTheSameHeaderValuesAreReturnedByTheDownstreamService(); } public override void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, [CallerMemberName] string headerKey = nameof(ThreadSafeHeadersTests)) { MapStatus_ResponseBody = ctx => ctx.Request.Headers[headerKey].ToString(); base.GivenThereIsAServiceRunningOn(port, statusCode, headerKey); } private void WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times, [CallerMemberName] string headerKey = nameof(ThreadSafeHeadersTests)) { var tasks = new Task[times]; for (var i = 0; i < times; i++) { var urlCopy = url; var rint = random.Next(0, 50); tasks[i] = GetForThreadSafeHeadersTest(urlCopy, rint, headerKey); } Task.WaitAll(tasks); } private async Task GetForThreadSafeHeadersTest(string url, int random, string headerKey) { var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Add(headerKey, [ random.ToString() ]); var response = await ocelotClient.SendAsync(request); var content = await response.Content.ReadAsStringAsync(); var result = int.Parse(content); var tshtr = new ThreadSafeHeadersTestResult(result, random); _results.Add(tshtr); } private void ThenTheSameHeaderValuesAreReturnedByTheDownstreamService() { foreach (var result in _results) { result.Result.ShouldBe(result.Random); } } private class ThreadSafeHeadersTestResult { public ThreadSafeHeadersTestResult(int result, int random) { Result = result; Random = random; } public int Result { get; } public int Random { get; } } } ================================================ FILE: test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Ocelot.Middleware; using System.Diagnostics; namespace Ocelot.AcceptanceTests; public class CustomMiddlewareTests : Steps { private int _counter; public CustomMiddlewareTests() { _counter = 0; } [Fact] public void Should_call_pre_query_string_builder_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { AuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_call_authorization_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { AuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_call_authentication_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { AuthenticationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/", "/41879/"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_call_pre_error_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_call_pre_authorization_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { PreAuthorizationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_call_pre_http_authentication_middleware() { var pipelineConfiguration = new OcelotPipelineConfiguration { PreAuthenticationMiddleware = async (ctx, next) => { _counter++; await next.Invoke(); }, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } [Fact] public void Should_not_throw_when_pipeline_terminates_early() { var pipelineConfiguration = new OcelotPipelineConfiguration { PreQueryStringBuilderMiddleware = (context, next) => Task.Run(() => { _counter++; return; // do not invoke the rest of the pipeline }), }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync(pipelineConfiguration)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => x.ThenTheCounterIs(1)) .BDDfy(); } /// /// This is just an example to show how you could hook into Ocelot pipeline with your own middleware. /// At the moment you must use Response.OnCompleted callback and cannot change the response :( /// I will see if this can be changed one day. /// [Fact] [Trait("Feat", "237")] // https://github.com/ThreeMammals/Ocelot/issues/237 [Trait("PR", "241")] // https://github.com/ThreeMammals/Ocelot/pull/241 [Trait("Release", "3.1.6")] // https://github.com/ThreeMammals/Ocelot/releases/tag/3.1.6 public void Should_fix_issue_237() { Func callback = state => { var context = (HttpContext)state; if (context.Response.StatusCode > 400) { Debug.WriteLine("COUNT CALLED"); Console.WriteLine("COUNT CALLED"); } return Task.CompletedTask; }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/", "/west"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOnPath(port, "/test")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithMiddlewareBeforePipeline(callback)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } private Task GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func middleware) => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot, async app => await app.UseMiddleware(middleware).UseOcelot()); private void ThenTheCounterIs(int expected) { _counter.ShouldBe(expected); } private void GivenThereIsAServiceRunningOnPath(int port, string basePath) { Task MapPath(HttpContext context) { if (string.IsNullOrEmpty(basePath)) { context.Response.StatusCode = (int)HttpStatusCode.OK; } else if (context.Request.Path.Value != basePath) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; } return Task.CompletedTask; } handler.GivenThereIsAServiceRunningOn(port, MapPath); } public class FakeMiddleware { private readonly RequestDelegate _next; private readonly Func _callback; public FakeMiddleware(RequestDelegate next, Func callback) { _next = next; _callback = callback; } public async Task Invoke(HttpContext context) { await _next(context); context.Response.OnCompleted(_callback, context); } } } ================================================ FILE: test/Ocelot.AcceptanceTests/DefaultVersionPolicyTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests; [Trait("Feat", "1672")] public sealed class DefaultVersionPolicyTests : Steps { public DefaultVersionPolicyTests() { } [Fact] public void Should_return_bad_gateway_when_request_higher_receive_lower() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrHigher); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } [Fact] public void Should_return_bad_gateway_when_request_lower_receive_higher() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrLower); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } [Fact] public void Should_return_bad_gateway_when_request_exact_receive_different() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionExact); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } [Fact] public void Should_return_ok_when_request_version_exact_receive_exact() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionExact); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_ok_when_request_version_lower_receive_lower() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_ok_when_request_version_lower_receive_exact() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "2.0", VersionPolicies.RequestVersionOrLower); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_ok_when_request_version_higher_receive_higher() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http2, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_ok_when_request_version_higher_receive_exact() { var port = PortFinder.GetRandomPort(); var route = GivenHttpsRoute(port, "1.1", VersionPolicies.RequestVersionOrHigher); var configuration = GivenConfiguration(route); var body = Body(); this.Given(x => GivenThereIsHttpsServiceRunningOn(port, HttpProtocols.Http1, body)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } private void GivenThereIsHttpsServiceRunningOn(int port, HttpProtocols protocols, [CallerMemberName] string body = "supercalifragilistic") { var url = DownstreamUrl(port, Uri.UriSchemeHttps); handler.GivenThereIsAServiceRunningOnWithKestrelOptions(url, string.Empty, options => options.ConfigureEndpointDefaults(listenOptions => { listenOptions.Protocols = protocols; }), context => { context.Response.StatusCode = (int)HttpStatusCode.OK; return context.Response.WriteAsync(body); }); } private static FileRoute GivenHttpsRoute(int port, string httpVersion, string versionPolicy) => new() { UpstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], DownstreamPathTemplate = "/", DownstreamHostAndPorts = new() { new("localhost", port) }, DownstreamScheme = Uri.UriSchemeHttps, // !!! DownstreamHttpVersion = httpVersion, DownstreamHttpVersionPolicy = versionPolicy, DangerousAcceptAnyServerCertificateValidator = true, }; } ================================================ FILE: test/Ocelot.AcceptanceTests/GzipTests.cs ================================================ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using System.IO.Compression; using System.Net.Http.Headers; using System.Text; namespace Ocelot.AcceptanceTests; public sealed class GzipTests : Steps { public GzipTests() { } [Fact] public void Should_return_response_200_with_simple_url() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Post); var configuration = GivenConfiguration(route); var input = "people"; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura", "\"people\"")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", GivenThePostHasGzipContent(input))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } private static StreamContent GivenThePostHasGzipContent(object input) { var json = JsonConvert.SerializeObject(input); var jsonBytes = Encoding.UTF8.GetBytes(json); var ms = new MemoryStream(); using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) { gzip.Write(jsonBytes, 0, jsonBytes.Length); } ms.Position = 0; var content = new StreamContent(ms); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); content.Headers.ContentEncoding.Add("gzip"); return content; } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody, string expected) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { if (context.Request.Headers.TryGetValue("Content-Encoding", out var contentEncoding)) { contentEncoding.First().ShouldBe("gzip"); string text = null; using (var decompress = new GZipStream(context.Request.Body, CompressionMode.Decompress)) { using var sr = new StreamReader(decompress); text = await sr.ReadToEndAsync(); } if (text != expected) { throw new Exception("not gzipped"); } context.Response.StatusCode = (int)statusCode; await context.Response.WriteAsync(responseBody); } else { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync("downstream path didnt match base path"); } }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; namespace Ocelot.AcceptanceTests; public sealed class HttpDelegatingHandlersTests : Steps { private string _downstreamPath; public HttpDelegatingHandlersTests() { } [Fact] public void Should_call_route_ordered_specific_handlers() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); route.DelegatingHandlers = new() { "FakeHandlerTwo", "FakeHandler", }; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheOrderedHandlersAreCalledCorrectly()) .BDDfy(); } [Fact] public void Should_call_global_di_handlers() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheHandlersAreCalledCorrectly()) .BDDfy(); } [Fact] public void Should_call_global_di_handlers_multiple_times() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithDelegatingHandler(true)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_call_global_di_handlers_with_dependency() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); var dependency = new FakeDependency(); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(dependency)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDependencyIsCalled(dependency)) .BDDfy(); } private static FileRoute GivenRoute(int port) => new() { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, DownstreamHostAndPorts = new() { new("localhost", port), }, UpstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], }; private void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() where THandler1 : DelegatingHandler where THandler2 : DelegatingHandler { GivenOcelotIsRunning(s => s .AddOcelot() .AddDelegatingHandler() .AddDelegatingHandler()); } private void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() where THandler1 : DelegatingHandler where THandler2 : DelegatingHandler { GivenOcelotIsRunning(s => s .AddOcelot() .AddDelegatingHandler(true) .AddDelegatingHandler(true)); } private void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) where THandler : DelegatingHandler { GivenOcelotIsRunning(s => s .AddSingleton(dependency) .AddOcelot() .AddDelegatingHandler(true)); } private static void ThenTheDependencyIsCalled(FakeDependency dependency) => dependency.Called.ShouldBeTrue(); private static void ThenTheHandlersAreCalledCorrectly() => FakeHandler.TimeCalled.ShouldBeLessThan(FakeHandlerTwo.TimeCalled); private static void ThenTheOrderedHandlersAreCalledCorrectly() => FakeHandlerTwo.TimeCalled.ShouldBeLessThan(FakeHandler.TimeCalled); public class FakeDependency { public bool Called; } // ReSharper disable once ClassNeverInstantiated.Local private class FakeHandlerWithDependency : DelegatingHandler { private readonly FakeDependency _dependency; public FakeHandlerWithDependency(FakeDependency dependency) { _dependency = dependency; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _dependency.Called = true; return base.SendAsync(request, cancellationToken); } } // ReSharper disable once ClassNeverInstantiated.Local private class FakeHandler : DelegatingHandler { public static DateTime TimeCalled { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellation) { TimeCalled = DateTime.Now; await Task.Delay(TimeSpan.FromMilliseconds(10), cancellation); return await base.SendAsync(request, cancellation); } } // ReSharper disable once ClassNeverInstantiated.Local private class FakeHandlerTwo : DelegatingHandler { public static DateTime TimeCalled { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellation) { TimeCalled = DateTime.Now; await Task.Delay(TimeSpan.FromMilliseconds(10), cancellation); return await base.SendAsync(request, cancellation); } } // ReSharper disable once ClassNeverInstantiated.Local private class FakeHandlerAgain : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellation) { Console.WriteLine(request.RequestUri); //do stuff and optionally call the base handler.. return await base.SendAsync(request, cancellation); } } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool match = _downstreamPath == basePath; context.Response.StatusCode = match ? (int)statusCode : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(match ? responseBody : nameof(HttpStatusCode.NotFound)); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/CookieStickySessionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.LoadBalancer.Balancers; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.LoadBalancer; [Trait("Feat", "336")] // https://github.com/ThreeMammals/Ocelot/pull/336 public sealed class CookieStickySessionsTests : Steps { private int[] _counters; #if NET9_0_OR_GREATER private static readonly Lock SyncLock = new(); #else private static readonly object SyncLock = new(); #endif public CookieStickySessionsTests() : base() { _counters = new int[2]; } [Fact] public void ShouldUseSameDownstreamHost_ForSingleRouteWithHighLoad() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route = GivenStickySessionsRoute([port1, port2]); var cookieName = route.LoadBalancerOptions.Key; var configuration = GivenConfiguration(route); this.Given(x => x.GivenProductServiceIsRunning(0, port1)) .Given(x => x.GivenProductServiceIsRunning(1, port2)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning()) .When(x => x.WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 10, cookieName, Guid.NewGuid().ToString())) .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 10)) // RoundRobin should return first service with port1 .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) .BDDfy(); } [Fact] public void ShouldUseDifferentDownstreamHost_ForDoubleRoutesWithDifferentCookies() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenStickySessionsRoute([port1, port2]); var cookieName = route1.LoadBalancerOptions.Key; var route2 = GivenStickySessionsRoute([port2, port1], "/test", cookieName + "bestid"); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenProductServiceIsRunning(0, port1)) .Given(x => x.GivenProductServiceIsRunning(1, port2)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning()) .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) // both cookies should have different values .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName + "bestid", "123")) // stick by cookie value .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 1)) .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 1)) .BDDfy(); } [Fact] public void ShouldUseSameDownstreamHost_ForDifferentRoutesWithSameCookie() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenStickySessionsRoute([port1, port2]); var cookieName = route1.LoadBalancerOptions.Key; var route2 = GivenStickySessionsRoute([port2, port1], "/test", cookieName); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenProductServiceIsRunning(0, port1)) .Given(x => x.GivenProductServiceIsRunning(1, port2)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning()) .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/", cookieName, "123")) .When(_ => WhenIGetUrlOnTheApiGatewayWithCookie("/test", cookieName, "123")) .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(0, 2)) .Then(x => x.ThenServiceShouldHaveBeenCalledTimes(1, 0)) .BDDfy(); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public async Task ShouldUseGlobalOptions_ForStaticRoutes() { _counters = new int[5]; var ports = PortFinder.GetPorts(2); var route1 = GivenStickySessionsRoute(ports); route1.LoadBalancerOptions = new(); // no load balancing -> use global opts var route2 = GivenStickySessionsRoute(ports.Reverse().ToArray(), "/test"); route1.LoadBalancerOptions = new(); // no load balancing -> use global opts var ports2 = PortFinder.GetPorts(2); var route3 = GivenStickySessionsRoute(ports2, "/nextSticky", CookieName() + "-nextSticky"); var port5 = PortFinder.GetRandomPort(); var route4 = GivenStickySessionsRoute([port5], "/noLoadBalancing"); // this route should not be overwritten by global LB opts route4.LoadBalancerOptions.Type = nameof(NoLoadBalancer); var configuration = GivenConfiguration(route1, route2, route3, route4); // static routes come to Routes collection configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(CookieStickySessions), Key = CookieName(), // !!! }; GivenProductServiceIsRunning(0, ports[0]); GivenProductServiceIsRunning(1, ports[1]); GivenProductServiceIsRunning(2, ports2[0]); GivenProductServiceIsRunning(3, ports2[1]); GivenProductServiceIsRunning(4, port5); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayWithCookie("/", CookieName(), "123"); await WhenIGetUrlOnTheApiGatewayWithCookie("/test", CookieName(), "123"); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/nextSticky", 5, CookieName() + "-nextSticky", "333"); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/noLoadBalancing", 7, "bla-bla-cookie", "bla-bla-value"); ThenServiceShouldHaveBeenCalledTimes(0, 2); ThenServiceShouldHaveBeenCalledTimes(1, 0); ThenServiceShouldHaveBeenCalledTimes(2, 5); ThenServiceShouldHaveBeenCalledTimes(3, 0); ThenServiceShouldHaveBeenCalledTimes(4, 7); } private static string CookieName([CallerMemberName] string cookieName = nameof(CookieStickySessionsTests)) => cookieName; private FileRoute GivenStickySessionsRoute(int[] ports, string upstream = null, [CallerMemberName] string cookieName = null) { var route = GivenRoute(ports[0], upstream: upstream ?? "/"); route.DownstreamHostAndPorts = ports.Select(Localhost).ToList(); route.LoadBalancerOptions = new() { Type = nameof(CookieStickySessions), Key = cookieName, // !!! Expiry = 300_000, }; return route; } private Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) { var tasks = new Task[times]; for (var i = 0; i < times; i++) { tasks[i] = GetParallelTask(url, cookie, value); } return Task.WhenAll(tasks); } private async Task GetParallelTask(string url, string cookie, string value) { var response = await WhenIGetUrlOnTheApiGateway(url, cookie, value); var content = await response.Content.ReadAsStringAsync(); var count = int.Parse(content); count.ShouldBeGreaterThan(0); } private void ThenServiceShouldHaveBeenCalledTimes(int index, int times) { _counters[index].ShouldBe(times); } private void GivenProductServiceIsRunning(int index, int port) { handler.GivenThereIsAServiceRunningOn(port, async context => { try { string response; lock (SyncLock) { _counters[index]++; response = _counters[index].ToString(); } context.Response.StatusCode = (int)HttpStatusCode.OK; await context.Response.WriteAsync(response); } catch (Exception exception) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await context.Response.WriteAsync(exception.StackTrace); } }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/ILoadBalancerAnalyzer.cs ================================================ using Ocelot.LoadBalancer; using Ocelot.Values; using System.Collections.Concurrent; namespace Ocelot.AcceptanceTests.LoadBalancer; public interface ILoadBalancerAnalyzer { string ServiceName { get; } string GenerationPrefix { get; } ConcurrentBag Events { get; } object Analyze(); Dictionary GetHostCounters(); Dictionary ToHostCountersDictionary(IEnumerable> grouping); bool HasManyServiceGenerations(int maxGeneration); int BottomOfConnections(); int TopOfConnections(); } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.AcceptanceTests.LoadBalancer; internal sealed class LeastConnectionAnalyzer : LoadBalancerAnalyzer, ILoadBalancer { private readonly LeastConnection loadBalancer; public LeastConnectionAnalyzer(Func>> services, string serviceName) : base(serviceName) { loadBalancer = new(services, serviceName); loadBalancer.Leased += Me_Leased; } private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); public override string Type => nameof(LeastConnectionAnalyzer); public override Task> LeaseAsync(HttpContext httpContext) => loadBalancer.LeaseAsync(httpContext); public override void Release(ServiceHostAndPort hostAndPort) => loadBalancer.Release(hostAndPort); public override Dictionary ToHostCountersDictionary(IEnumerable> grouping) => grouping.ToDictionary(g => g.Key, g => g.Count(e => e.Lease == g.Key)); } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/LeastConnectionAnalyzerCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.AcceptanceTests.LoadBalancer; internal sealed class LeastConnectionAnalyzerCreator : ILoadBalancerCreator { // We need to adhere to the same implementations of RoundRobinCreator, which results in a significant design overhead, (until redesigned) public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { var loadBalancer = new LeastConnectionAnalyzer( serviceProvider.GetAsync, !string.IsNullOrEmpty(route.ServiceName) // if service discovery mode then use service name; otherwise use balancer key ? route.ServiceName : route.LoadBalancerKey); return new OkResponse(loadBalancer); } public string Type => nameof(LeastConnectionAnalyzer); } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerAnalyzer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; using System.Collections.Concurrent; namespace Ocelot.AcceptanceTests.LoadBalancer; internal class LoadBalancerAnalyzer : ILoadBalancerAnalyzer, ILoadBalancer { protected readonly string _serviceName; protected LoadBalancerAnalyzer(string serviceName) => _serviceName = serviceName; public string ServiceName => _serviceName; public virtual string GenerationPrefix => "Gen:"; public ConcurrentBag Events { get; } = new(); public virtual object Analyze() { var allGenerations = Events .Select(e => e.Service.Tags.FirstOrDefault(t => t.StartsWith(GenerationPrefix))) .Where(generation => !string.IsNullOrEmpty(generation)) .Distinct().ToArray(); var allIndices = Events.Select(e => e.ServiceIndex) .Distinct().OrderBy(index => index).ToArray(); Dictionary> eventsPerGeneration = new(); foreach (var generation in allGenerations) { var l = Events.Where(e => e.Service.Tags.Contains(generation)).ToList(); eventsPerGeneration.Add(generation, l); } Dictionary> generationIndices = new(); foreach (var generation in allGenerations) { var l = eventsPerGeneration[generation].Select(e => e.ServiceIndex).Distinct().ToList(); generationIndices.Add(generation, l); } Dictionary> generationLeases = new(); foreach (var generation in allGenerations) { var l = eventsPerGeneration[generation].Select(e => e.Lease).ToList(); generationLeases.Add(generation, l); } Dictionary> generationHosts = new(); foreach (var generation in allGenerations) { var l = eventsPerGeneration[generation].Select(e => e.Lease.HostAndPort).Distinct().ToList(); generationHosts.Add(generation, l); } Dictionary> generationLeasesWithMaxConnections = new(); foreach (var generation in allGenerations) { List leases = new(); var uniqueHosts = generationHosts[generation]; foreach (var host in uniqueHosts) { int max = generationLeases[generation].Where(l => l == host).Max(l => l.Connections); Lease wanted = generationLeases[generation].Find(l => l == host && l.Connections == max); leases.Add(wanted); } leases = leases.OrderBy(l => l.HostAndPort.DownstreamPort).ToList(); generationLeasesWithMaxConnections.Add(generation, leases); } return generationLeasesWithMaxConnections; } public virtual bool HasManyServiceGenerations(int maxGeneration) { int[] generations = new int[maxGeneration + 1]; string[] tags = new string[maxGeneration + 1]; for (int i = 0; i < generations.Length; i++) { generations[i] = i; tags[i] = GenerationPrefix + i; } var all = Events .Select(e => e.Service.Tags.FirstOrDefault(t => t.StartsWith(GenerationPrefix))) .Distinct().ToArray(); return all.All(tags.Contains); } public virtual Dictionary GetHostCounters() { var hosts = Events.Select(e => e.Lease.HostAndPort).Distinct().ToList(); var grouping = Events .GroupBy(e => e.Lease.HostAndPort) .OrderBy(g => g.Key.DownstreamPort); return ToHostCountersDictionary(grouping); } public virtual Dictionary ToHostCountersDictionary(IEnumerable> grouping) => grouping.ToDictionary(g => g.Key, g => g.Count(e => e.Lease == g.Key)); public virtual int BottomOfConnections() { var hostCounters = GetHostCounters(); return hostCounters.Min(_ => _.Value); } public virtual int TopOfConnections() { var hostCounters = GetHostCounters(); return hostCounters.Max(_ => _.Value); } public virtual string Type => nameof(LoadBalancerAnalyzer); public virtual Task> LeaseAsync(HttpContext httpContext) => Task.FromResult>(new ErrorResponse(new UnableToFindLoadBalancerError(GetType().Name))); public virtual void Release(ServiceHostAndPort hostAndPort) { } } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/LoadBalancerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.AcceptanceTests.LoadBalancer; public sealed class LoadBalancerTests : ConcurrentSteps { [Theory] [Trait("Feat", "211")] [InlineData(false)] // original scenario, clean config [InlineData(true)] // extended scenario using analyzer public void ShouldLoadBalanceRequestWithLeastConnection(bool withAnalyzer) { var ports = PortFinder.GetPorts(2); var route = GivenLbRoute(ports, withAnalyzer ? nameof(LeastConnectionAnalyzer) : nameof(LeastConnection)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); LeastConnectionAnalyzer lbAnalyzer = null; LeastConnectionAnalyzer getAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider) { //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse return lbAnalyzer ??= new LeastConnectionAnalyzerCreator().Create(route, provider)?.Data as LeastConnectionAnalyzer; } Action withLeastConnectionAnalyzer = (s) => s.AddOcelot().AddCustomLoadBalancer(getAnalyzer); GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(withAnalyzer ? withLeastConnectionAnalyzer : WithAddOcelot)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 99)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(99)) .And(x => ThenAllServicesCalledOptimisticAmountOfTimes(lbAnalyzer)) .And(x => ThenServiceCountersShouldMatchLeasingCounters(lbAnalyzer, ports, 99)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes( #if NET10_0_OR_GREATER Bottom(99, ports.Length) - 3, Top(99, ports.Length) + 3 #else Bottom(99, ports.Length), Top(99, ports.Length) #endif )) // .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 49)) // strict assertion, this is ideal case when load is not high .And(x => _counters.ShouldAllBe(c => #if NET10_0_OR_GREATER c <= 53 && c >= 46, #else c == 50 || c == 49, #endif CalledTimesMessage())) // LeastConnection algorithm distributes counters as 49/50 or 50/49 depending on thread synchronization .BDDfy(); } [Theory] [Trait("Bug", "365")] [InlineData(false)] // original scenario, clean config [InlineData(true)] // extended scenario using analyzer public void ShouldLoadBalanceRequestWithRoundRobin(bool withAnalyzer) { var ports = PortFinder.GetPorts(2); var route = GivenLbRoute(ports, withAnalyzer ? nameof(RoundRobinAnalyzer) : nameof(RoundRobin)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); RoundRobinAnalyzer lbAnalyzer = null; RoundRobinAnalyzer getAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider) { //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse return lbAnalyzer ??= new RoundRobinAnalyzerCreator().Create(route, provider)?.Data as RoundRobinAnalyzer; } Action withRoundRobinAnalyzer = (s) => s.AddOcelot().AddCustomLoadBalancer(getAnalyzer); GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(withAnalyzer ? withRoundRobinAnalyzer : WithAddOcelot)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 99)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(99)) .And(x => ThenAllServicesCalledOptimisticAmountOfTimes(lbAnalyzer)) .And(x => ThenServiceCountersShouldMatchLeasingCounters(lbAnalyzer, ports, 99)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(99, ports.Length), Top(99, ports.Length))) .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 49)) // strict assertion .BDDfy(); } [Fact] [Trait("Feat", "961")] public void ShouldLoadBalanceRequestWithCustomLoadBalancer() { Func loadBalancerFactoryFunc = (serviceProvider, route, discoveryProvider) => new CustomLoadBalancer(discoveryProvider.GetAsync); var ports = PortFinder.GetPorts(2); var route = GivenLbRoute(ports, nameof(CustomLoadBalancer)); var configuration = GivenConfiguration(route); var downstreamServiceUrls = ports.Select(DownstreamUrl).ToArray(); Action withCustomLoadBalancer = (s) => s.AddOcelot().AddCustomLoadBalancer(loadBalancerFactoryFunc); GivenMultipleServiceInstancesAreRunning(downstreamServiceUrls); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(withCustomLoadBalancer)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 50)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(50, ports.Length), Top(50, ports.Length))) .And(x => ThenServicesShouldHaveBeenCalledTimes(25, 25)) // strict assertion .BDDfy(); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void ShouldApplyGlobalOptions_ForStaticRoutes() { var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute(ports1, upstream: "/route1"); route1.LoadBalancerOptions = new(); // no load balancing -> use global opts var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute(ports2, nameof(LeastConnection), "/route2"); var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), "/noLoadBalancing"); var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); WhenIGetUrlOnTheApiGatewayConcurrently("/route1", 2); WhenIGetUrlOnTheApiGatewayConcurrently("/route2", 5); WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing", 7); ThenServicesShouldHaveBeenCalledTimes(1, 1, 3, 2, 7, 0); // main assertion, explanation is below ThenServiceShouldHaveBeenCalledTimes(0, 1); // RoundRobin for 2 ThenServiceShouldHaveBeenCalledTimes(1, 1); // RoundRobin for 2 ThenServiceShouldHaveBeenCalledTimes(2, 3); // LeastConnection for 5 ThenServiceShouldHaveBeenCalledTimes(3, 2); // LeastConnection for 5 ThenServiceShouldHaveBeenCalledTimes(4, 7); // NoLoadBalancer for 7 ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 7 } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void ShouldApplyGlobalGroupOptions_ForStaticRoutes_WhenRouteOptsHasAKey() { // 1st route var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute(ports1, upstream: "/route1"); route1.LoadBalancerOptions = null; // 1st route is not balanced route1.Key = null; // 1st route is not in the global group // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute(ports2, upstream: "/route2"); route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones route2.Key = "R2"; // 2nd route is in the group // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute(ports3, nameof(NoLoadBalancer), "/noLoadBalancing"); var configuration = GivenConfiguration(route1, route2, route3); configuration.GlobalConfiguration.LoadBalancerOptions = new() { RouteKeys = ["R2"], Type = nameof(RoundRobin), }; var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); WhenIGetUrlOnTheApiGatewayConcurrently("/route1", 2); WhenIGetUrlOnTheApiGatewayConcurrently("/route2", 4); WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing", 5); ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2 ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2 ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4 ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4 ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5 ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5 } private sealed class CustomLoadBalancer : ILoadBalancer { private readonly Func>> _services; #if NET9_0_OR_GREATER private static readonly Lock _lock = new(); #else private static readonly object _lock = new(); #endif private int _last; public string Type => nameof(CustomLoadBalancer); public CustomLoadBalancer(Func>> services) => _services = services; public async Task> LeaseAsync(HttpContext httpContext) { var services = await _services(); lock (_lock) { if (_last >= services.Count) _last = 0; var next = services[_last++]; return new OkResponse(next.HostAndPort); } } public void Release(ServiceHostAndPort hostAndPort) { } } private FileRoute GivenLbRoute(int[] ports, string loadBalancer = null, string upstream = null) { var route = GivenRoute(ports[0], upstream: upstream); route.DownstreamHostAndPorts = ports.Select(Localhost).ToList(); route.LoadBalancerOptions = new(loadBalancer ?? nameof(LeastConnection)); return route; } } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzer.cs ================================================ using KubeClient.Models; using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.AcceptanceTests.LoadBalancer; internal sealed class RoundRobinAnalyzer : LoadBalancerAnalyzer, ILoadBalancer { private readonly RoundRobin loadBalancer; public RoundRobinAnalyzer(Func>> services, string serviceName) : base(serviceName) { loadBalancer = new(services, serviceName); loadBalancer.Leased += Me_Leased; } private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); public override string Type => nameof(RoundRobinAnalyzer); public override Task> LeaseAsync(HttpContext httpContext) => loadBalancer.LeaseAsync(httpContext); public override void Release(ServiceHostAndPort hostAndPort) => loadBalancer.Release(hostAndPort); public override string GenerationPrefix => nameof(EndpointsV1.Metadata.Generation) + ":"; public override Dictionary ToHostCountersDictionary(IEnumerable> grouping) => grouping.ToDictionary(g => g.Key, g => g.Max(e => e.Lease.Connections)); } ================================================ FILE: test/Ocelot.AcceptanceTests/LoadBalancer/RoundRobinAnalyzerCreator.cs ================================================ using Ocelot.Configuration; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.AcceptanceTests.LoadBalancer; internal sealed class RoundRobinAnalyzerCreator : ILoadBalancerCreator { // We need to adhere to the same implementations of RoundRobinCreator, which results in a significant design overhead, (until redesigned) public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) { var loadBalancer = new RoundRobinAnalyzer( serviceProvider.GetAsync, !string.IsNullOrEmpty(route.ServiceName) // if service discovery mode then use service name; otherwise use balancer key ? route.ServiceName : route.LoadBalancerKey); return new OkResponse(loadBalancer); } public string Type => nameof(RoundRobinAnalyzer); } ================================================ FILE: test/Ocelot.AcceptanceTests/LogLevelTests.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Logging; using Ocelot.Middleware; using Serilog; using Serilog.Core; namespace Ocelot.AcceptanceTests; public sealed class LogLevelTests : Steps { private readonly string _logFileName; private readonly string _appSettingsFileName; private const string AppSettingsFormat = "{{\"Logging\":{{\"LogLevel\":{{\"Default\":\"{0}\",\"System\":\"{0}\",\"Microsoft\":\"{0}\"}}}}}}"; public LogLevelTests() { _logFileName = $"ocelot_logs_{TestID}.log"; _appSettingsFileName = $"appsettings_{TestID}.json"; Files.Add(_logFileName); Files.Add(_appSettingsFileName); } private void ThenMessagesAreLogged(string[] notAllowedMessageTypes, string[] allowedMessageTypes) { var logFilePath = GetLogFilePath(); var logFileContent = File.ReadAllText(logFilePath); var logFileLines = logFileContent.Split(Environment.NewLine); var logFileLinesWithLogLevel = logFileLines.Where(x => notAllowedMessageTypes.Any(x.Contains)).ToList(); logFileLinesWithLogLevel.Count.ShouldBe(0); var logFileLinesWithAllowedLogLevel = logFileLines.Where(x => allowedMessageTypes.Any(x.Contains)).ToList(); logFileLinesWithAllowedLogLevel.Count.ShouldBe(2 * allowedMessageTypes.Length); } private void TestFactory(string[] notAllowedMessageTypes, string[] allowedMessageTypes, LogLevel level) { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = ["Get"], RequestIdKey = "Oc-RequestId", }, }, }; using var logger = GetLogger(level); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningWithMinimumLogLevel(logger, _appSettingsFileName)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => logger.Dispose()) .Then(x => ThenMessagesAreLogged(notAllowedMessageTypes, allowedMessageTypes)) .BDDfy(); } private Task GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appsettingsFileName) => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot, async app => { app.Use(async (context, next) => { var loggerFactory = context.RequestServices.GetService(); var ocelotLogger = loggerFactory.CreateLogger(); ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); await next.Invoke(); }); await app.UseOcelot(); }, null, host => host.ConfigureLogging(l => l.ClearProviders().AddSerilog(logger)), null, null); [Fact] public void If_minimum_log_level_is_critical_then_only_critical_messages_are_logged() => TestFactory( [ "TRACE", "INFORMATION", "WARNING", "ERROR" ], [ "CRITICAL" ], LogLevel.Critical); [Fact] public void If_minimum_log_level_is_error_then_critical_and_error_are_logged() => TestFactory( [ "TRACE", "INFORMATION", "WARNING", "DEBUG" ], [ "CRITICAL", "ERROR" ], LogLevel.Error); [Fact] public void If_minimum_log_level_is_warning_then_critical_error_and_warning_are_logged() => TestFactory( [ "TRACE", "INFORMATION", "DEBUG" ], [ "CRITICAL", "ERROR", "WARNING" ], LogLevel.Warning); [Fact] public void If_minimum_log_level_is_information_then_critical_error_warning_and_information_are_logged() => TestFactory( [ "TRACE", "DEBUG" ], [ "CRITICAL", "ERROR", "WARNING", "INFORMATION" ], LogLevel.Information); [Fact] public void If_minimum_log_level_is_debug_then_critical_error_warning_information_and_debug_are_logged() => TestFactory( [ "TRACE" ], [ "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" ], LogLevel.Debug); [Fact] public void If_minimum_log_level_is_trace_then_critical_error_warning_information_debug_and_trace_are_logged() => TestFactory( [], [ "TRACE", "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" ], LogLevel.Trace); private Logger GetLogger(LogLevel logLevel) { var logFilePath = GetLogFilePath(); UpdateAppSettings(logLevel); var logger = logLevel switch { LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.Debug => new LoggerConfiguration().MinimumLevel.Debug() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() .WriteTo.File(logFilePath) .CreateLogger(), LogLevel.None => new LoggerConfiguration() .WriteTo.File(logFilePath) .CreateLogger(), _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null), }; return logger; } private void UpdateAppSettings(LogLevel logLevel) { var appSettingsFilePath = Path.Combine(AppContext.BaseDirectory, _appSettingsFileName); if (File.Exists(appSettingsFilePath)) { File.Delete(appSettingsFilePath); } var appSettings = string.Format(AppSettingsFormat, Enum.GetName(typeof(LogLevel), logLevel)); File.WriteAllText(appSettingsFilePath, appSettings); } private string GetLogFilePath() { var logFilePath = Path.Combine(AppContext.BaseDirectory, _logFileName); return logFilePath; } private void GivenThereIsAServiceRunningOn(int port) { handler.GivenThereIsAServiceRunningOn(port, context => { context.Response.StatusCode = 200; return context.Response.WriteAsync(string.Empty); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Logging/MemoryLogger.cs ================================================ using Microsoft.Extensions.Logging; using System.Collections.Concurrent; using System.Text; namespace Ocelot.AcceptanceTests.Logging; public class MemoryLogger : ILogger { public readonly ConcurrentQueue _messages = new(); public readonly ConcurrentQueue _exceptions = new(); public IReadOnlyCollection Messages => _messages; public IReadOnlyCollection Exceptions => _exceptions; public string Logbook => string.Join(Environment.NewLine, _messages); public IDisposable BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { if (state is null) return; var message = formatter?.Invoke(state, exception); if (message == null) return; if (exception is not null) { var builder = new StringBuilder() .AppendLine(message) .Append(exception.ToString()); _messages.Enqueue(builder.ToString()); _exceptions.Enqueue(exception); } else { _messages.Enqueue(message); } } } ================================================ FILE: test/Ocelot.AcceptanceTests/Logging/TestLoggerFactory.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; namespace Ocelot.AcceptanceTests.Logging; public class TestLoggerFactory : IOcelotLoggerFactory { private readonly ILoggerFactory _factory; private readonly IRequestScopedDataRepository _repository; private readonly MemoryLogger _logger; private readonly OcelotLogger _ologger; private bool _disposed; public TestLoggerFactory(ILoggerFactory factory, IRequestScopedDataRepository repository) { _factory = factory; _repository = repository; _logger = new(); _ologger = new(_logger, _repository); } public MemoryLogger Logger => _logger; public IOcelotLogger CreateLogger() => _ologger; protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _factory?.Dispose(); } _disposed = true; } ~TestLoggerFactory() => Dispose(false); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Metadata; using Ocelot.Middleware; using System.Globalization; namespace Ocelot.AcceptanceTests.Metadata; [Trait("Feat", "738")] public sealed class DownstreamMetadataTests : Steps { public enum StringArrayConfig { Default = 1, AlternateSeparators, AlternateTrimChars, AlternateStringSplitOptions, Mix, } public enum NumberConfig { Default = 1, AlternateNumberStyle, AlternateCulture, } public DownstreamMetadataTests() { } [Theory] [InlineData(typeof(StringDownStreamMetadataHandler))] [InlineData(typeof(StringArrayDownStreamMetadataHandler))] [InlineData(typeof(BoolDownStreamMetadataHandler))] [InlineData(typeof(DoubleDownStreamMetadataHandler))] [InlineData(typeof(SuperDataContainerDownStreamMetadataHandler))] public void ShouldMatchTargetObjects(Type currentType) { (Dictionary sourceDictionary, Dictionary _) = GetSourceAndTargetDictionary(currentType); var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = [ Localhost(port) ], DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = ["Get"], Metadata = sourceDictionary, DelegatingHandlers = [ currentType.Name ], }, }, }; this.Given(x => handler.GivenThereIsAServiceRunningOn(port, MapOK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(currentType)) .When(x => WhenIGetUrlOnTheApiGateway($"/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } /// /// Testing the string array type with different configurations. /// /// The possible separators. /// The trimmed characters. /// If the empty entries should be removed. /// The current test configuration. [Theory] [InlineData(new[] { "," }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.Default)] [InlineData( new[] { ";", ".", "," }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.AlternateSeparators)] [InlineData( new[] { "," }, new[] { ' ', ';', ':' }, nameof(StringSplitOptions.None), StringArrayConfig.AlternateTrimChars)] [InlineData( new[] { "," }, new[] { ' ' }, nameof(StringSplitOptions.RemoveEmptyEntries), StringArrayConfig.AlternateStringSplitOptions)] [InlineData( new[] { ";", ".", "," }, new[] { ' ', '_', ':' }, nameof(StringSplitOptions.RemoveEmptyEntries), StringArrayConfig.Mix)] public void ShouldMatchTargetStringArrayAccordingToConfiguration( string[] separators, char[] trimChars, string stringSplitOption, StringArrayConfig currentConfig) { (Dictionary sourceDictionary, Dictionary _) = GetSourceAndTargetDictionariesForStringArrayType(currentConfig); sourceDictionary.Add(nameof(StringArrayConfig), currentConfig.ToString()); var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = [ Localhost(port) ], DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = ["Get"], Metadata = sourceDictionary, DelegatingHandlers = [ nameof(StringArrayDownStreamMetadataHandler) ], }, }, GlobalConfiguration = new FileGlobalConfiguration { MetadataOptions = new() { Separators = separators, TrimChars = trimChars, StringSplitOption = stringSplitOption, }, }, }; this.Given(x => handler.GivenThereIsAServiceRunningOn(port, MapOK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(StringArrayDownStreamMetadataHandler))) .When(x => WhenIGetUrlOnTheApiGateway($"/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Theory] [InlineData(NumberStyles.Any, "de-CH", NumberConfig.Default)] [InlineData(NumberStyles.AllowParentheses | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowLeadingSign, "de-CH", NumberConfig.AlternateNumberStyle)] public void ShouldMatchTargetNumberAccordingToConfiguration( NumberStyles numberStyles, string cultureName, NumberConfig currentConfig) { (Dictionary sourceDictionary, Dictionary _) = GetSourceAndTargetDictionariesForNumberType(); sourceDictionary.Add(nameof(NumberConfig), currentConfig.ToString()); var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = [ Localhost(port) ], DownstreamScheme = "http", UpstreamPathTemplate = "/", UpstreamHttpMethod = ["Get"], Metadata = sourceDictionary, DelegatingHandlers = [nameof(IntDownStreamMetadataHandler)], }, }, GlobalConfiguration = new() { MetadataOptions = new() { NumberStyle = numberStyles.ToString(), CurrentCulture = cultureName, }, }, }; GivenThereIsAServiceRunningOn(port); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(IntDownStreamMetadataHandler))) .When(x => WhenIGetUrlOnTheApiGateway($"/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } /// /// Starting ocelot with the delegating handler of type currentType. /// /// The current delegating handler type. /// Throws if delegating handler type doesn't match. private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) { switch (currentType) { case { } t when t == typeof(StringDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; case { } t when t == typeof(StringArrayDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; case { } t when t == typeof(BoolDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; case { } t when t == typeof(DoubleDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; case { } t when t == typeof(IntDownStreamMetadataHandler): GivenOcelotIsRunningWithDelegatingHandler(); break; default: throw new NotImplementedException(); } } // It would have been better to use a generic method, but it is not possible to use a generic type as a parameter // for the delegating handler name private class StringDownStreamMetadataHandler : DownstreamMetadataHandler { public StringDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { } } private class StringArrayDownStreamMetadataHandler : DownstreamMetadataHandler { public StringArrayDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( httpContextAccessor) { } } private class BoolDownStreamMetadataHandler : DownstreamMetadataHandler { public BoolDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { } } private class DoubleDownStreamMetadataHandler : DownstreamMetadataHandler { public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { } } private class IntDownStreamMetadataHandler : DownstreamMetadataHandler { public IntDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { } } private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler { public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( httpContextAccessor) { } } /// /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route /// and checking if the extension method GetMetadata returns the correct value. /// /// The current type. private class DownstreamMetadataHandler : DelegatingHandler { private readonly IHttpContextAccessor _httpContextAccessor; public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute(); if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(StringArrayConfig))) { var currentConfig = Enum.Parse(downstreamRoute.MetadataOptions.Metadata[nameof(StringArrayConfig)]); downstreamRoute.MetadataOptions.Metadata.Remove(nameof(StringArrayConfig)); (Dictionary _, Dictionary targetDictionary) = GetSourceAndTargetDictionariesForStringArrayType(currentConfig); foreach (var key in targetDictionary.Keys) { Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); } } else if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(NumberConfig))) { downstreamRoute.MetadataOptions.Metadata.Remove(nameof(NumberConfig)); (Dictionary _, Dictionary targetDictionary) = GetSourceAndTargetDictionariesForNumberType(); foreach (var key in targetDictionary.Keys) { Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); } } else { (Dictionary _, Dictionary targetDictionary) = GetSourceAndTargetDictionary(typeof(T)); foreach (var key in targetDictionary.Keys) { Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); } } return base.SendAsync(request, cancellationToken); } } public static (Dictionary SourceDictionary, Dictionary TargetDictionary) GetSourceAndTargetDictionariesForStringArrayType(StringArrayConfig currentConfig) { Dictionary sourceDictionary; Dictionary targetDictionary; if (currentConfig == StringArrayConfig.Default) { sourceDictionary = new Dictionary { { "Key1", "Value1, Value2, Value3" }, { "Key2", "Value2, Value3, Value4" }, { "Key3", "Value3, ,Value4, Value5" }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } if (currentConfig == StringArrayConfig.AlternateSeparators) { sourceDictionary = new Dictionary { { "Key1", "Value1; Value2. Value3" }, { "Key2", "Value2. Value3, Value4" }, { "Key3", "Value3, ,Value4; Value5" }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } if (currentConfig == StringArrayConfig.AlternateTrimChars) { sourceDictionary = new Dictionary { { "Key1", "Value1; :, Value2 :, Value3 " }, { "Key2", " Value2, Value3; , Value4" }, { "Key3", "Value3 , ,Value4, Value5 " }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } if (currentConfig == StringArrayConfig.AlternateStringSplitOptions) { sourceDictionary = new Dictionary { { "Key1", "Value1, ,Value2, Value3, " }, { "Key2", "Value2, , ,Value3, Value4, , ," }, { "Key3", "Value3, ,Value4, , ,Value5" }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } if (currentConfig == StringArrayConfig.Mix) { sourceDictionary = new Dictionary { { "Key1", "Value1; :, Value2. :, Value3 " }, { "Key2", " Value2_, , , Value3; , Value4" }, { "Key3", "Value3:; , ,Value4, Value5 " }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } throw new NotImplementedException(); } public static (Dictionary SourceDictionary, Dictionary TargetDictionary) GetSourceAndTargetDictionariesForNumberType() { return ( new Dictionary { { "Key1", "-2" }, { "Key2", " (1000000) " }, { "Key3", "-1000000000 " }, }, new Dictionary { { "Key1", -2 }, { "Key2", -1000000 }, { "Key3", -1000000000 } }); } /// /// Method retrieving the source and target dictionary for the current type. /// The source value is of type string and the target is of type object. /// /// The current type. /// A source and a target directory to compare the results. /// Throws if type not found. public static (Dictionary SourceDictionary, Dictionary TargetDictionary) GetSourceAndTargetDictionary(Type currentType) { Dictionary sourceDictionary; Dictionary targetDictionary; if (currentType == typeof(StringDownStreamMetadataHandler) || currentType == typeof(string)) { sourceDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; targetDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; return (sourceDictionary, targetDictionary); } if (currentType == typeof(StringArrayDownStreamMetadataHandler) || currentType == typeof(string[])) { sourceDictionary = new Dictionary { { "Key1", "Value1, Value2, Value3" }, { "Key2", "Value2, Value3, Value4" }, { "Key3", "Value3, ,Value4, Value5" }, }; targetDictionary = new Dictionary { { "Key1", new[] { "Value1", "Value2", "Value3" } }, { "Key2", new[] { "Value2", "Value3", "Value4" } }, { "Key3", new[] { "Value3", "Value4", "Value5" } }, }; return (sourceDictionary, targetDictionary); } if (currentType == typeof(BoolDownStreamMetadataHandler) || currentType == typeof(bool?)) { sourceDictionary = new Dictionary { { "Key1", "true" }, { "Key2", "false" }, { "Key3", "null" }, { "Key4", "disabled" }, { "Key5", "0" }, { "Key6", "1" }, { "Key7", "yes" }, { "Key8", "enabled" }, { "Key9", "on" }, { "Key10", "off" }, { "Key11", "test" }, }; targetDictionary = new Dictionary { { "Key1", true }, { "Key2", false }, { "Key3", null }, { "Key4", false }, { "Key5", false }, { "Key6", true }, { "Key7", true }, { "Key8", true }, { "Key9", true }, { "Key10", false }, { "Key11", null }, }; return (sourceDictionary, targetDictionary); } if (currentType == typeof(DoubleDownStreamMetadataHandler) || currentType == typeof(double)) { sourceDictionary = new Dictionary { { "Key1", "0.00001" }, { "Key2", "0.00000001" }, }; targetDictionary = new Dictionary { { "Key1", 0.00001 }, { "Key2", 0.00000001 }, }; return (sourceDictionary, targetDictionary); } if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler) || currentType == typeof(SuperDataContainer)) { sourceDictionary = new Dictionary { { "Key1", "{\"key1\":\"Bonjour\",\"key2\":\"Hello\",\"key3\":0.00001,\"key4\":true}" }, }; targetDictionary = new Dictionary { { "Key1", new SuperDataContainer { Key1 = "Bonjour", Key2 = "Hello", Key3 = 0.00001, Key4 = true, } }, }; return (sourceDictionary, targetDictionary); } throw new NotImplementedException(); } public class SuperDataContainer { public string Key1 { get; set; } public string Key2 { get; set; } public double Key3 { get; set; } public bool? Key4 { get; set; } public override bool Equals(object obj) { // Check for null and compare run-time types. if (obj == null || this.GetType() != obj.GetType()) { return false; } SuperDataContainer other = (SuperDataContainer)obj; return Key1 == other.Key1 && Key2 == other.Key2 && Key3.Equals(other.Key3) && Key4 == other.Key4; } // https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode public override int GetHashCode() { unchecked { int hash = 17; hash = (hash * 23) + (Key1?.GetHashCode() ?? 0); hash = (hash * 23) + (Key2?.GetHashCode() ?? 0); hash = (hash * 23) + Key3.GetHashCode(); hash = (hash * 23) + (Key4?.GetHashCode() ?? 0); return hash; } } } } ================================================ FILE: test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj ================================================  0.0.0-dev net8.0;net9.0;net10.0 disable disable false true Ocelot.AcceptanceTests Exe true win-x64;osx-x64 false false false True ..\..\codeanalysis.ruleset $(NoWarn);CS0618;CS1591 PreserveNewest PreserveNewest PreserveNewest Always runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: test/Ocelot.AcceptanceTests/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following set of attributes. // Change these attribute values to modify the information associated with an assembly. [assembly: AssemblyCompany("Three Mammals")] [assembly: AssemblyCopyright("© 2026 Three Mammals. MIT licensed OSS.")] [assembly: AssemblyProduct("Ocelot Gateway")] [assembly: AssemblyTrademark("Ocelot")] // Setting ComVisible to false makes the types in this assembly not visible to COM components. // If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("f8c224fe-36be-45f5-9b0e-666d8f4a9b52")] ================================================ FILE: test/Ocelot.AcceptanceTests/Properties/BddfyConfig.cs ================================================ using System.Collections.Concurrent; using TestStack.BDDfy.Configuration; namespace Ocelot.AcceptanceTests.Properties; public static class BddfyConfig { public static void Configure() { //// Configurator.Processors.ConsoleReport.RunsOn(story => story.Result != Result.Passed); //Configurator.Processors.ConsoleReport.Disable(); //Configurator.Processors.Add(() => new BddfyProcessor()); ////Configurator.BatchProcessors.Add(new BddfyBatchProcessingReporter()); //Configurator.BatchProcessors.HtmlReport.Disable(); } } public class BddfyProcessor : IProcessor { private static readonly ConcurrentDictionary Cache = new(); public ProcessType ProcessType => ProcessType.Report; public void Process(Story story) { //Console.WriteLine($"{story.Result} Story: {story.Namespace} | Total Scenarios: {story.Scenarios.Count()}"); foreach (var scenario in story.Scenarios) { if (Cache.TryAdd(scenario.Id, scenario)) { Console.ForegroundColor = scenario.Result == Result.Passed ? ConsoleColor.Green : ConsoleColor.Red; Console.Write(scenario.Result); Console.ForegroundColor = ConsoleColor.Yellow; Console.Write($" {scenario.Id}: "); Console.ForegroundColor = ConsoleColor.Blue; Console.Write(scenario.Title); Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine($", in {scenario.Duration.TotalSeconds} sec"); Console.ResetColor(); } } } } public class BddfyBatchProcessingReporter : IBatchProcessor { private static int totalStories; private static int totalScenarios; private static Result final = Result.NotExecuted; public static void Process(Story story) { //foreach (var scenario in story.Scenarios) //{ // //Console.WriteLine($"Scenario: {scenario.Title} - Status: {scenario.Result}"); // totalScenarios++; //} totalScenarios += story.Scenarios.Count(); totalStories++; final = (Result)Math.Max((int)story.Result, (int)final); } public void Process(IEnumerable stories) { var list = stories.ToList(); list.ForEach(Process); Console.WriteLine("Warning: Per-scenario logging has been disabled!"); Console.WriteLine($"The {nameof(BddfyBatchProcessingReporter)} has processed total {totalStories} stories with total {totalScenarios} scenarios."); Console.WriteLine($"Final result: {final}"); Console.WriteLine(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Properties/GlobalSuppressions.cs ================================================ // This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. using System.Diagnostics.CodeAnalysis; ================================================ FILE: test/Ocelot.AcceptanceTests/QualityOfService/PollyQoSTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.AcceptanceTests.Configuration; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Polly; using Polly.CircuitBreaker; using Polly.Timeout; using System.Runtime.CompilerServices; using TimeoutStrategy = Ocelot.Provider.Polly.TimeoutStrategy; namespace Ocelot.AcceptanceTests.QualityOfService; [Trait("Feat", "23")] // https://github.com/ThreeMammals/Ocelot/issues/23 [Trait("Feat", "39")] // https://github.com/ThreeMammals/Ocelot/pull/39 public sealed class PollyQoSTests : PollyQosSteps { [Fact] [Trait("Feat", "318")] // https://github.com/ThreeMammals/Ocelot/issues/318 [Trait("PR", "319")] // https://github.com/ThreeMammals/Ocelot/pull/319 public async Task Should_not_timeout() { var qos = new QoSOptions() { BreakDuration = 500, MinimumThroughput = 10, FailureRatio = 0.5, SamplingDuration = 5, Timeout = 1000, // !!! }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos, method: HttpMethods.Post); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, timeout: 10); // !!! GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIPostUrlOnTheApiGateway("/", "postContent"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); } [Fact] [Trait("Feat", "318")] // https://github.com/ThreeMammals/Ocelot/issues/318 [Trait("PR", "319")] // https://github.com/ThreeMammals/Ocelot/pull/319 public async Task Should_timeout() { var qos = new QoSOptions(1000); // timeout var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos, method: HttpMethods.Post); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, timeout: 2100); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIPostUrlOnTheApiGateway("/", "postContent"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); } [Fact] [Trait("Bug", "1550")] // https://github.com/ThreeMammals/Ocelot/issues/1550 [Trait("Bug", "1706")] // https://github.com/ThreeMammals/Ocelot/issues/1706 [Trait("PR", "1753")] // https://github.com/ThreeMammals/Ocelot/pull/1753 public async Task Should_open_circuit_breaker_after_two_exceptions() { var qos = new QoSOptions(2, 1000) { Timeout = 100_000, // infinite -> actually no timeout }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos); var configuration = GivenConfiguration(route); GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); for (int i = 0; i < qos.MinimumThroughput.Value; i++) { await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); } await WhenIGetUrlOnTheApiGateway("/"); // opened ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Polly status } [Fact] [Trait("Bug", "2085")] // https://github.com/ThreeMammals/Ocelot/issues/2085 public async Task Should_open_circuit_breaker_for_DefaultBreakDuration() { int cicdMs = IsCiCd() ? 50 : 0; int invalidDuration = CircuitBreakerStrategy.LowBreakDuration; // valid value must be >500ms, exact 500ms is invalid var qos = new QoSOptions(2, invalidDuration) { Timeout = 100_000, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos); var configuration = GivenConfiguration(route); GivenThereIsABrokenServiceRunningOn(port, HttpStatusCode.InternalServerError); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); await WhenIGetUrlOnTheApiGateway("/"); // opened ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Polly status await GivenIWaitMilliseconds(CircuitBreakerStrategy.DefaultBreakDuration - 500); // 5000 - 500 = 4500; BreakDuration is not elapsed await WhenIGetUrlOnTheApiGateway("/"); // still opened ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // still opened GivenThereIsABrokenServiceOnline(HttpStatusCode.NotFound); await GivenIWaitMilliseconds(500 + cicdMs); // BreakDuration should elapse now await WhenIGetUrlOnTheApiGateway("/"); // closed, service online ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound); // closed, service online ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.NotFound)); } /// /// This test, written by Tom, is based on a object generated by the . /// The objects are handled by the predicate, which is part of the default implementation of the . /// /// A representing the asynchronous acceptance test. [Fact] [Trait("PR", "39")] // https://github.com/ThreeMammals/Ocelot/pull/39 public async Task Should_open_circuit_breaker_then_close() { var qos = new QoSOptions(CircuitBreakerStrategy.LowMinimumThroughput, CircuitBreakerStrategy.LowBreakDuration + 1) // 501 { Timeout = 1000, // -> TimeoutRejectedException }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); const int MillisecondsDelay = 2_100; GivenThereIsAPossiblyBrokenServiceRunningOn(port, "Hello from Laura", MillisecondsDelay); await WhenIGetUrlOnTheApiGateway("/"); await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, "Hello from Laura"); await WhenIGetUrlOnTheApiGateway("/"); // repeat same request because min MinimumThroughput is 2 await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, "Hello from Laura"); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await GivenIWaitMilliseconds(MillisecondsDelay); await WhenIGetUrlOnTheApiGateway("/"); await ThenTheResponseShouldBeAsync(HttpStatusCode.OK, "Hello from Laura"); } [Fact] // [SkippableFact] [Trait("PR", "39")] // https://github.com/ThreeMammals/Ocelot/pull/39 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public async Task Should_open_circuit_breaker_then_close_without_timeout_strategy() { //Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), SkippingOnMacOS); var qos = new QoSOptions(CircuitBreakerStrategy.LowMinimumThroughput, 1000) // 501 { Timeout = null, // switch off timeout strategy }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, qos); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await TestRouteCircuitBreaker([port], route.UpstreamPathTemplate, route.QoSOptions); } [Fact] // [SkippableFact] [Trait("PR", "39")] // https://github.com/ThreeMammals/Ocelot/pull/39 public async Task Open_circuit_should_not_effect_different_route() { // Skip.If(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), SkippingOnMacOS); var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var qos1 = new QoSOptions(2, CircuitBreakerStrategy.LowBreakDuration + 1) // 501 { Timeout = 1000, }; var route = GivenRoute(port1, qos1); var route2 = GivenRoute(port2, new(), "/working"); var configuration = GivenConfiguration(route, route2); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); const int MillisecondsDelay = 2_100; GivenThereIsAPossiblyBrokenServiceRunningOn(port1, "Hello from Laura", MillisecondsDelay); GivenThereIsAServiceRunningOn(port2, HttpStatusCode.OK, 0, "Hello from Tom"); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("Hello from Laura"); await WhenIGetUrlOnTheApiGateway("/"); // repeat same request because min MinimumThroughput is 2 ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("Hello from Laura"); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await WhenIGetUrlOnTheApiGateway("/working"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("Hello from Tom"); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await GivenIWaitMilliseconds(3000); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("Hello from Laura"); } // TODO: If failed in parallel execution mode, switch to SequentialTests // This issue may arise when transitioning all tests to parallel execution // This test must be sequential because of usage of the static DownstreamRoute.DefaultTimeoutSeconds [Fact] [Trait("Bug", "1833")] // https://github.com/ThreeMammals/Ocelot/issues/1833 public async Task Should_timeout_per_default_after_90_seconds() { try { DownstreamRoute.DefaultTimeoutSeconds = 3; // override original value var defTimeoutMs = Ms(DownstreamRoute.DefaultTimeoutSeconds); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, new(new FileQoSOptions())); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, defTimeoutMs + 500); // 3.5s > 3s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 3 secs -> Timeout exception aka request cancellation } finally { DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; } } [Fact] [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 public async Task HasRouteAndGlobalTimeouts_RouteTimeoutShouldTakePrecedenceOverGlobalTimeout() { const int RouteTimeoutSeconds = 2, GlobalTimeoutSeconds = 4; int serviceTimeoutMs = Ms(Math.Max(RouteTimeoutSeconds, GlobalTimeoutSeconds)) + 500; // total 4.5 sec var port = PortFinder.GetRandomPort(); var qos = new FileQoSOptions() { TimeoutValue = Ms(RouteTimeoutSeconds) }; var route = GivenRoute(port, new(qos)); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = Ms(GlobalTimeoutSeconds) }; // !!! GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, serviceTimeoutMs); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); var watcher = await WatchWhenIGetUrlOnTheApiGateway(); ThenTimeoutIsInRange(watcher, Ms(RouteTimeoutSeconds), Ms(RouteTimeoutSeconds) + 500); // (2.0, 2.5) s ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); await ThenTheResponseBodyShouldBeAsync(string.Empty); } [Fact] [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 public async Task HasGlobalTimeoutOnly_ForAllRoutesGlobalTimeoutShouldTakePrecedenceOverAbsoluteGlobalTimeout() { const int GlobalTimeoutSeconds = 2; int serviceTimeoutMs = Ms(GlobalTimeoutSeconds + 1); // total 3 sec var ports = PortFinder.GetPorts(2); FileRoute route1 = GivenRoute(ports[0], "/route1"), route2 = GivenRoute(ports[1], "/route2"); // without QoS timeouts var configuration = GivenConfiguration(route1, route2); configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = Ms(GlobalTimeoutSeconds) }; // !!! GivenThereIsAServiceRunningOn(ports[0], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable GivenThereIsAServiceRunningOn(ports[1], HttpStatusCode.OK, serviceTimeoutMs); // 2s -> ServiceUnavailable GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); var watchers = await Task.WhenAll( WatchWhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate), WatchWhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate)); int globalTimeoutMs = Ms(GlobalTimeoutSeconds); foreach (var watcher in watchers) { ThenTimeoutIsInRange(watcher, globalTimeoutMs, Ms(DownstreamRoute.DefaultTimeoutSeconds)); // (2.0, 90) so assert roughly ThenTimeoutIsInRange(watcher, globalTimeoutMs, globalTimeoutMs + 500); // (2.0, 2.5) so assert precisely ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // after 2 secs -> TimeoutException by TimeoutDelegatingHandler await ThenTheResponseBodyShouldBeAsync(string.Empty); } } [Fact] [Trait("PR", "2081")] // https://github.com/ThreeMammals/Ocelot/pull/2081 [Trait("Feat", "2080")] // https://github.com/ThreeMammals/Ocelot/issues/2080 public async Task HasRouteAndGlobalFailureRatios_RouteFailureRatioShouldTakePrecedenceOverGlobalFailureRatio() { const double RouteFailureRatio = 0.50D, GlobalFailureRatio = 0.75D; var qos = new FileQoSOptions() { ExceptionsAllowedBeforeBreaking = 3, // after 3 actions FailureRatio is activated DurationOfBreak = CircuitBreakerStrategy.LowBreakDuration + 1, FailureRatio = RouteFailureRatio, // 50% of requests SamplingDuration = 1_000, }; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, new(qos)); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new() { FailureRatio = GlobalFailureRatio }; // !!! int count = 0; bool isOK = false; GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, () => 10, () => !isOK && ++count % 2 == 0); // 1 of 2 fails GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 1 -> 0% await WhenIGetUrlOnTheApiGateway("/"); // fail ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 1 failed of 2 -> 50% but failure ratio is ignored because of 2 actions < 3 await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 1 failed of 3 -> 33% await WhenIGetUrlOnTheApiGateway("/"); // fail ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 2 failed of 4 -> 50% -> circuit is open now! await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 2 failed of 5 -> 40%, but circuit is already open await WhenIGetUrlOnTheApiGateway("/"); // fail ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 3 failed of 6 -> 50%, but circuit is already open count.ShouldBe(4); // 2 of 4 were failed, and the service was called 4 times isOK = true; // the next requests should be OK int cicdMs = IsCiCd() ? 50 : 0; await GivenIWaitMilliseconds(qos.DurationOfBreak.Value + cicdMs); // breaking period is over, thus, circuit breaker is closed await WhenIGetUrlOnTheApiGateway("/"); // OK but circuit is closed ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // circuit is closed await ThenTheResponseBodyShouldBeAsync(nameof(HasRouteAndGlobalFailureRatios_RouteFailureRatioShouldTakePrecedenceOverGlobalFailureRatio)); } [Fact] [Trait("PR", "2081")] // https://github.com/ThreeMammals/Ocelot/pull/2081 [Trait("Feat", "2080")] // https://github.com/ThreeMammals/Ocelot/issues/2080 public async Task HasGlobalFailureRatioOnly_GlobalFailureRatioShouldTakePrecedenceOverPollyDefaultFailureRatio() { const double GlobalFailureRatio = 0.75D; // Polly def FailureRatio is CircuitBreakerStrategy.DefaultFailureRatio -> 0.1 -> 10% var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); // without failure ratios var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new() { ExceptionsAllowedBeforeBreaking = 2, // after 2 actions FailureRatio is activated DurationOfBreak = CircuitBreakerStrategy.LowBreakDuration + 1, FailureRatio = GlobalFailureRatio, // 75% of requests SamplingDuration = 1_000, }; // !!! int count = 0; bool isOK = false; GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, () => 10, () => !isOK && ++count > 2); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 1 -> 0% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // 0 failed of 2 -> 0% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 1 failed of 3 -> 33% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 2 failed of 4 -> 50% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 3 failed of 5 -> 60% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 4 failed of 6 -> 66% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 5 failed of 7 -> 71% await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError); // 6 failed of 8 -> 75% -> circuit is open now! await WhenIGetUrlOnTheApiGateway("/"); // Assert circuit is open await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 7 failed of 9 -> 77%, but circuit is already open await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // 8 failed of 10 -> 80%, but circuit is already open count.ShouldBe(8); // the service was called 8 times of 10 total isOK = true; // the next requests should be OK int cicdMs = IsCiCd() ? 50 : 0; await GivenIWaitMilliseconds(configuration.GlobalConfiguration.QoSOptions.DurationOfBreak.Value + cicdMs); // breaking period is over, thus, circuit breaker is closed await WhenIGetUrlOnTheApiGateway("/"); // OK but circuit is closed ThenTheStatusCodeShouldBe(HttpStatusCode.OK); // circuit is closed await ThenTheResponseBodyShouldBeAsync(nameof(HasGlobalFailureRatioOnly_GlobalFailureRatioShouldTakePrecedenceOverPollyDefaultFailureRatio)); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public async Task ShouldApplyGlobalQosOptions_ForStaticRoutes() { const int GlobalTimeout = 1500; const int RouteExceptions = 2, GlobalExceptions = 3; const int RouteBreakMs = 1000, GlobalBreakMs = 2000; var ports = PortFinder.GetPorts(3); var route1 = GivenRoute(ports[0], options: null, // no opts -> use global opts "/route1"); var route2 = GivenRoute(ports[1], new QoSOptions(RouteExceptions, RouteBreakMs), "/route2"); var route3 = GivenRoute(ports[2], new QoSOptions(0, 0) { Timeout = GlobalTimeout }, // disable Circuit Breaker via disallowing of global opts to substitute "/noCircuitBreaker"); var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection var globalOptions = configuration.GlobalConfiguration.QoSOptions = new(new QoSOptions(GlobalExceptions, GlobalBreakMs)); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); // TODO: Add acceptance steps that are more parallelism-friendly. // The code below failed due to a shared response object being used for sequential steps. //await Task.WhenAll( // TestRouteCircuitBreaker(route1, 0, globalOptions), // test global scenario // TestRouteCircuitBreaker(route2, 1), // test route-level scenario // TestRouteTimeout(route3)); await TestRouteCircuitBreaker([ports[0]], route1.UpstreamPathTemplate, globalOptions, 0); // test global scenario await TestRouteCircuitBreaker([ports[1]], route2.UpstreamPathTemplate, route2.QoSOptions, 1); // test route-level scenario await TestRouteTimeout([ports[2]], route3.UpstreamPathTemplate, route3.QoSOptions); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public async Task ShouldApplyGlobalQosOptions_ForStaticRoutes_WithGroupedOpts() { const int GlobalTimeout = 1500, GlobalExceptions = 3, GlobalBreakMs = 2000; var ports = PortFinder.GetPorts(3); // 1st route var route1 = GivenRoute(ports[0], options: null, // no opts -> no QoS at all "/route1"); route1.Key = null; // 1st route is not in the global group // 2nd route var route2 = GivenRoute(ports[1], options: null, // 2nd route opts will be applied from global ones "/route2"); route2.Key = "R2"; // 2nd route is in the group // 3rd route var route3 = GivenRoute(ports[2], new QoSOptions(0, 0) { Timeout = GlobalTimeout }, // disable Circuit Breaker via disallowing of global opts to substitute "/noCircuitBreaker"); var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection var globalOptions = configuration.GlobalConfiguration.QoSOptions = new(new QoSOptions(GlobalExceptions, GlobalBreakMs)) { RouteKeys = ["R2"], }; GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningWithPolly(); await TestRouteCircuitBreaker([ports[0]], route1.UpstreamPathTemplate, route1.QoSOptions, 0); // no QoS scenario GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, 0); // bring 1st service back online await WhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate) .ContinueWith(t => ThenTheResponseShouldBeAsync(HttpStatusCode.OK, "OK")); await TestRouteCircuitBreaker([ports[1]], route2.UpstreamPathTemplate, globalOptions, 1); // test global scenario await TestRouteTimeout([route3.DownstreamHostAndPorts[0].Port], route3.UpstreamPathTemplate, route3.QoSOptions); } private FileRoute GivenRoute(int port, QoSOptions options, string upstream = null, string method = null) { var route = GivenRoute(port, upstream, upstream); route.UpstreamHttpMethod = [method ?? HttpMethods.Get]; route.QoSOptions = options is null ? null : new(options); return route; } private Task GivenOcelotIsRunningWithPolly() => GivenOcelotIsRunningAsync(WithPolly); private static void WithPolly(IServiceCollection services) => services.AddOcelot().AddPolly(); private static Task GivenIWaitMilliseconds(int ms) => GivenIWaitAsync(ms); private void GivenThereIsAPossiblyBrokenServiceRunningOn(int port, string responseBody, int millisecondsDelay, int requestNo = 2) { int requestCount = 0; handler.GivenThereIsAServiceRunningOn(port, async context => { if (requestCount == requestNo) { // In Polly v8: // MinimumThroughput (exceptions) must be 2 or more // BreakDuration (ex. DurationOfBreak) must be > 500 // Timeout (ex. TimeoutValue) must be 1000 or more // So, we wait for 2.1 seconds to make sure the circuit is open // BreakDuration * MinimumThroughput + Timeout // 500 * 2 + 1000 = 2000 minimum + 100 milliseconds to exceed the minimum await Task.Delay(millisecondsDelay); // 2_100 } requestCount++; context.Response.StatusCode = (int)HttpStatusCode.OK; await context.Response.WriteAsync(responseBody); }); } protected override void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, int timeout, [CallerMemberName] string response = nameof(PollyQoSTests)) => base.GivenThereIsAServiceRunningOn(port, statusCode, timeout, response); } public class PollyQosSteps : TimeoutTestsBase, IQosSteps, IDisposable { private readonly QosSteps steps; public PollyQosSteps() => steps = new(this); public override void Dispose() { steps.Dispose(); base.Dispose(); GC.SuppressFinalize(this); } public void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false) => steps.GivenThereIsABrokenServiceOnline(onlineStatusCode, index, length, isDiscovery); public void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0) => steps.GivenThereIsABrokenServiceRunningOn(port, brokenStatusCode, index); public void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, Func timeoutStrategy, Func failingStrategy, [CallerMemberName] string response = null) => steps.GivenThereIsAServiceRunningOn(port, statusCode, timeoutStrategy, failingStrategy, response); public Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false) => steps.TestRouteCircuitBreaker(ports, upstreamPath, qos, index, isDiscovery); public Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos) => steps.TestRouteTimeout(ports, upstreamPath, qos); } ================================================ FILE: test/Ocelot.AcceptanceTests/QualityOfService/QosSteps.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Provider.Polly; using System.Collections.Concurrent; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.QualityOfService; public class QosSteps : Steps, IQosSteps { private readonly Steps self; public QosSteps(Steps self) => this.self = self; public async Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false) { qos ??= new(); await handler.ReleasePortAsync(ports) .ContinueWith(t => self.ReleasePortAsync(ports)); int count = PollyQoSResiliencePipelineProvider.DefaultServerErrorCodes.Count; HttpStatusCode[] codes = PollyQoSResiliencePipelineProvider.DefaultServerErrorCodes.ToArray(); HttpStatusCode nextBadStatus = codes[DateTime.Now.Millisecond % count]; for (int i = 0; i < ports.Length; i++) { GivenThereIsABrokenServiceRunningOn(ports[i], nextBadStatus, index); } for (int i = 0; qos.MinimumThroughput.HasValue && i < qos.MinimumThroughput.Value; i++) { nextBadStatus = codes[DateTime.Now.Millisecond % count]; GivenThereIsABrokenServiceOnline(nextBadStatus, index, isDiscovery: isDiscovery); await self.WhenIGetUrlOnTheApiGateway(upstreamPath); await self.ThenTheResponseShouldBeAsync(nextBadStatus, nextBadStatus.ToString()); } if (qos.MinimumThroughput.HasValue && qos.MinimumThroughput > 0) { GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, index, isDiscovery: isDiscovery); await self.WhenIGetUrlOnTheApiGateway(upstreamPath); self.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // Circuit is open GivenThereIsABrokenServiceOnline(HttpStatusCode.OK, index, isDiscovery: isDiscovery); int cicdMs = IsCiCd() ? 100 : 0; await GivenIWaitAsync(qos.BreakDuration.Value + cicdMs); // Wait until the circuit is either half-open or closed await self.WhenIGetUrlOnTheApiGateway(upstreamPath); await self.ThenTheResponseShouldBeAsync(HttpStatusCode.OK, "OK"); } } public async Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos) { int counter = 0; bool notFailing() => false; int firstHasTimeout() { int count = Interlocked.Increment(ref counter), timeout = qos.Timeout.Value; return count <= 1 ? timeout + 100 : timeout / 2; } await handler.ReleasePortAsync(ports) .ContinueWith(t => self.ReleasePortAsync(ports)); for (int i = 0; i < ports.Length; i++) { GivenThereIsAServiceRunningOn(ports[i], HttpStatusCode.OK, firstHasTimeout, notFailing); } await self.WhenIGetUrlOnTheApiGateway(upstreamPath); self.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); // OnTimeout await self.WhenIGetUrlOnTheApiGateway(upstreamPath); await self.ThenTheResponseShouldBeAsync(HttpStatusCode.OK); } public Action CounterStrategy { get; set; } public void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, Func timeoutStrategy, Func failingStrategy, [CallerMemberName] string response = null) { Task MapBodyWithTimeout(HttpContext context) { int delayMs = timeoutStrategy(); bool failed = failingStrategy(); HttpStatusCode status = failed ? HttpStatusCode.InternalServerError : statusCode; context.Response.StatusCode = (int)status; CounterStrategy?.Invoke(port); return Task.Delay(delayMs) .ContinueWith(t => context.Response.WriteAsync(response)); } handler.GivenThereIsAServiceRunningOn(port, MapBodyWithTimeout); } public ConcurrentDictionary BrokenServiceStatusCode = new(); public void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0) { GivenThereIsABrokenServiceOnline(brokenStatusCode, index); handler.GivenThereIsAServiceRunningOn(port, context => { var code = BrokenServiceStatusCode[index]; context.Response.StatusCode = (int)code; CounterStrategy?.Invoke(port); return context.Response.WriteAsync(code.ToString()); }); } public void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false) { if (!isDiscovery) { BrokenServiceStatusCode[index] = onlineStatusCode; } else { foreach (var kv in BrokenServiceStatusCode) BrokenServiceStatusCode[kv.Key] = onlineStatusCode; } } } public interface IQosSteps { Task TestRouteCircuitBreaker(int[] ports, string upstreamPath, FileQoSOptions qos, int index = 0, bool isDiscovery = false); Task TestRouteTimeout(int[] ports, string upstreamPath, FileQoSOptions qos); void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, Func timeoutStrategy, Func failingStrategy, [CallerMemberName] string response = null); void GivenThereIsABrokenServiceRunningOn(int port, HttpStatusCode brokenStatusCode, int index = 0); void GivenThereIsABrokenServiceOnline(HttpStatusCode onlineStatusCode, int index = 0, int length = 1, bool isDiscovery = false); } ================================================ FILE: test/Ocelot.AcceptanceTests/RateLimiting/ClientHeaderRateLimitingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; using Ocelot.RateLimiting; using System.Runtime.InteropServices; namespace Ocelot.AcceptanceTests.RateLimiting; public sealed class ClientHeaderRateLimitingTests : RateLimitingSteps { const int OK = (int)HttpStatusCode.OK; const int TooManyRequests = (int)HttpStatusCode.TooManyRequests; private int _counter; public ClientHeaderRateLimitingTests() { } [Fact] [Trait("Feat", "37")] public async Task Should_call_with_rate_limiting() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, limit: 3, period: "1s", periodTimespan: 1); // -> 3/1s/w1s, so, periods are equal var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); ThenTheStatusCodeShouldBeOK(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 2); ThenTheStatusCodeShouldBeOK(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); ThenTheStatusCodeShouldBe(TooManyRequests); } [Fact] [Trait("Feat", "37")] public async Task Should_wait_for_period_timespan_to_elapse_before_making_next_request() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, downstream: "/api/ClientRateLimit?count={count}", upstream: "/ClientRateLimit/?{count}", limit: 3, period: "1s", periodTimespan: 1); // -> 3/1s/w1s var configuration = GivenConfiguration(route); _counter = 0; GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); ThenTheStatusCodeShouldBeOK(); GivenIWait(50); await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 2 ThenTheStatusCodeShouldBeOK(); GivenIWait(50); await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 3 ThenTheStatusCodeShouldBeOK(); GivenIWait(50); await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 4, exceeded with 150ms shift ThenTheStatusCodeShouldBe(TooManyRequests); GivenIWait(500); // half of wait window await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 5 ThenTheStatusCodeShouldBe(TooManyRequests); GivenIWait(500 + 5); // wait window has elapsed await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 6->1 ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("4"); // total 4 OK responses } private int _count = 0; private int Count() => ++_count; private string Url() => $"/ClientRateLimit/?{Count()}"; private async Task WhenIGetUrlOnTheApiGatewayMultipleTimes(Func urlDelegate, long times) { for (long i = 0; i < times; i++) { var url = urlDelegate.Invoke(); await WhenIGetUrlOnTheApiGatewayMultipleTimes(url, 1); } } [Fact] [Trait("Feat", "37")] public async Task Should_call_middleware_with_white_list_client() { const int Limit = 3; const string ClientID = "ocelotclient1"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, whitelist: [ClientID], limit: Limit, period: "3s", periodTimespan: 2); // main period is greater than wait window one var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); var responses = await WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader("/ClientRateLimit", Limit + 1, route.RateLimitOptions.ClientIdHeader.IfEmpty(configuration.GlobalConfiguration.RateLimitOptions.ClientIdHeader), ClientID); ThenTheStatusCodeShouldBeOK(); responses.Length.ShouldBe(Limit + 1); responses.ShouldAllBe(response => response.StatusCode == HttpStatusCode.OK); var bodies = responses.Select(r => r.Content.ReadAsStringAsync().Result).ToList(); bodies.Sum(int.Parse).ShouldBe(10); // n * (n + 1) / 2 -> 4*5/2 -> 20/2 bodies.Sort(); bodies.ForEach(body => int.Parse(body).ShouldBe(bodies.IndexOf(body) + 1)); // 1, 2, 3, 4 } [Fact] [Trait("Bug", "1590")] public async Task StatusShouldNotBeEqualTo429_PeriodTimespanValueIsGreaterThanPeriod() { _counter = 0; // Bug scenario const string period = "1s"; const double periodTimespan = /*30*/3; // but decrease 30 to 3 secs, "no wasting time" life hack const long limit = 100L; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/ClientRateLimit?count={count}", "/ClientRateLimit/?{count}", new(), limit, period, periodTimespan); // bug scenario, adapted var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); // main scenario await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, limit); // 100 times to reach the limit ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe(route.RateLimitOptions.Limit.ToString()); // total 100 OK responses // extra scenario await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); // 101st request should fail ThenTheStatusCodeShouldBe(TooManyRequests); GivenIWait((int)TimeSpan.FromSeconds(periodTimespan).TotalMilliseconds); // in 3 secs Wait will elapse await WhenIGetUrlOnTheApiGatewayMultipleTimes(Url, 1); ThenTheStatusCodeShouldBeOK(); ThenTheResponseBodyShouldBe("101"); // total 101 OK responses } [Theory] [Trait("Bug", "1305")] [InlineData(false)] [InlineData(true)] public async Task Should_set_ratelimiting_headers_on_response_when_EnableHeaders_set_to(bool enableHeaders) { int port = PortFinder.GetRandomPort(); var route = GivenRoute(port, limit: 3, period: "100s", periodTimespan: 1000.0D); // 3/100s/w1000.00s route.RateLimitOptions.EnableHeaders = enableHeaders; var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); ThenTheStatusCodeShouldBeOK(); ThenRateLimitingHeadersExistInResponse(enableHeaders); ThenTheResponseHeaderExists(HeaderNames.RetryAfter, false); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 2); ThenTheStatusCodeShouldBeOK(); ThenRateLimitingHeadersExistInResponse(enableHeaders); ThenTheResponseHeaderExists(HeaderNames.RetryAfter, false); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); ThenTheStatusCodeShouldBe(TooManyRequests); ThenRateLimitingHeadersExistInResponse(false); ThenTheResponseHeaderExists(HeaderNames.RetryAfter, enableHeaders); } [Fact] [Trait("Feat", "37")] [Trait("Feat", "585")] [Trait("PR", "2294")] public async Task Should_block_unknown_clients_by_writing_warning_to_body_with_503_status() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, limit: 3, period: "1s", periodTimespan: 1); // -> 3/1s/w1s var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader("/ClientRateLimit", 1, "bla-bla-header", "spy"); ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable); ThenTheResponseBodyShouldBe("Rate limiting client could not be identified for the route '/ClientRateLimit' due to a missing or unknown client ID header required by rule '3/1s/w1s'!"); ThenTheResponseHeaderExists(HeaderNames.RetryAfter).ShouldBe("-1"); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "1915")] // https://github.com/ThreeMammals/Ocelot/issues/1915 public async Task Should_apply_global_options_when_there_are_no_route_opts() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); route.RateLimitOptions = null; // !!! var configuration = GivenConfiguration(route); var global = configuration.GlobalConfiguration.RateLimitOptions; global.Limit = 3; global.Period = "1s"; global.Wait = "500ms"; GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 3); // 3 ThenTheStatusCodeShouldBeOK(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); // 4, exceeding ThenTheStatusCodeShouldBe(TooManyRequests); int halfOfWaitWindow = 250; GivenIWait(halfOfWaitWindow); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); // 5 ThenTheStatusCodeShouldBe(TooManyRequests); ThenTheResponseBodyShouldBe("Exceeding!"); var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter); if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS Assert.True(retryAfter.StartsWith("0.1") || retryAfter.StartsWith("0.2")); else retryAfter.ShouldStartWith("0.2"); // 0.2xx //var seconds = double.Parse(retryAfter); //int theRestOfMilliseconds = (int)(1000 * seconds); /* theRestOfMilliseconds.ShouldBeInRange(200, halfOfWaitWindow); */ GivenIWait(halfOfWaitWindow); // the end of wait period await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); // 1, new counting period has started ThenTheStatusCodeShouldBeOK(); } [Fact] [Trait("Feat", "1229")] // https://github.com/ThreeMammals/Ocelot/issues/1229 public async Task Should_apply_group_global_options_when_route_opts_has_a_key() { // 1st route var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream: "/rateUnlimited"); route.RateLimitOptions = null; // 1st route is not limited route.Key = null; // 1st route is not in the global group // 2nd route var port2 = PortFinder.GetRandomPort(); var route2 = GivenRoute(port2, downstream: "/api/ClientRateLimit2?count={count}", upstream: "/rateLimited/?{count}"); route2.RateLimitOptions = null; // 2nd route opts will be applied from global ones route2.Key = "R2"; // 2nd route is in the group var configuration = GivenConfiguration(route, route2); var global = configuration.GlobalConfiguration.RateLimitOptions; global.RouteKeys = ["R2"]; global.Limit = 3; global.Period = "1s"; global.Wait = "500ms"; GivenThereIsAServiceRunningOn(port); GivenThereIsAServiceRunningOn(port2, "/api/ClientRateLimit2", MapOK); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); // Make requests to the 1st unlimited route var responses = await WhenIGetUrlOnTheApiGatewayMultipleTimes("/rateUnlimited", (int)global.Limit + 1); ThenTheStatusCodeShouldBeOK(); responses.Length.ShouldBe((int)global.Limit + 1); responses.ShouldAllBe(response => response.StatusCode == HttpStatusCode.OK); var bodies = responses.Select(r => r.Content.ReadAsStringAsync().Result).ToList(); bodies.ForEach(b => b.ShouldBe(Body())); // Make requests to the 2nd rate-limited route await WhenIGetUrlOnTheApiGatewayMultipleTimes("/rateLimited/", 3); // 3 ThenTheStatusCodeShouldBeOK(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/rateLimited/", 1); // 4, exceeding ThenTheStatusCodeShouldBe(TooManyRequests); int halfOfWaitWindow = 250; GivenIWait(halfOfWaitWindow); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/rateLimited/", 1); // 5 ThenTheStatusCodeShouldBe(TooManyRequests); ThenTheResponseBodyShouldBe("Exceeding!"); var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter); if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS Assert.True(retryAfter.StartsWith("0.1") || retryAfter.StartsWith("0.2")); else retryAfter.ShouldStartWith("0.2"); // 0.2xx var seconds = double.Parse(retryAfter); int theRestOfMilliseconds = (int)(1000 * seconds); /* theRestOfMilliseconds.ShouldBeInRange(200, halfOfWaitWindow); */ GivenIWait(halfOfWaitWindow); // the end of wait period await WhenIGetUrlOnTheApiGatewayMultipleTimes("/rateLimited/", 1); // 1, new counting period has started ThenTheStatusCodeShouldBeOK(); } [Fact] [Trait("PR", "2294")] public async Task Should_rate_limit_using_sliding_period_without_wait_period() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); route.RateLimitOptions = new(route.RateLimitOptions) { Limit = 3, Period = "1s", PeriodTimespan = null, Wait = string.Empty, // No wait window -> sliding period in fixed window aka Period is 1s }; // rule -> 3/1s/w0 var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOnPath(port, "/api/ClientRateLimit"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 3); ThenTheStatusCodeShouldBeOK(); await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); ThenTheStatusCodeShouldBe(TooManyRequests); ThenTheResponseBodyShouldBe("Exceeding!"); var retryAfter = ThenTheResponseHeaderExists(HeaderNames.RetryAfter); retryAfter.ShouldStartWith("0.9"); // 0.9xx int theRestOfMilliseconds = (int)(1000 * double.Parse(retryAfter)); theRestOfMilliseconds.ShouldBeGreaterThan(900); // Mutual behavior arises from test instability, which is sensitive to consumed CPU resources and thread synchronization in CI/CD environments int slidingPeriodEndsInMs = Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true" // check GitHub CI-CD context ? (int)RateLimitRule.ParseTimespan(route.RateLimitOptions.Period).TotalMilliseconds // not strict requirement for CI-CD, ensure the test is stable : theRestOfMilliseconds; // otherwise it is strict in local dev env, but somethimes the test fails :D GivenIWait(slidingPeriodEndsInMs); // the end of sliding period await WhenIGetUrlOnTheApiGatewayMultipleTimes("/ClientRateLimit", 1); // 1, new counting period has started ThenTheStatusCodeShouldBeOK(); } private void ThenRateLimitingHeadersExistInResponse(bool headersExist) { response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Limit).ShouldBe(headersExist); response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Remaining).ShouldBe(headersExist); response.Headers.Contains(RateLimitingHeaders.X_RateLimit_Reset).ShouldBe(headersExist); } protected override Task MapOK(HttpContext context) { int count = Interlocked.Increment(ref _counter); // thread-safe analog of _counter++ context.Response.StatusCode = OK; return context.Response.WriteAsync(count.ToString()); } private FileRoute GivenRoute(int port, string downstream = null, string upstream = null, List whitelist = null, long? limit = null, string period = null, double? periodTimespan = null) { var route = base.GivenRoute(port, upstream ?? "/ClientRateLimit", downstream ?? "/api/ClientRateLimit"); route.RequestIdKey = "Oc-RequestId"; route.RateLimitOptions = new() { ClientWhitelist = whitelist, Limit = limit ?? 3, Period = period.IfEmpty("1s"), PeriodTimespan = periodTimespan ?? 1D, }; return route; } public override FileConfiguration GivenConfiguration(params FileRoute[] routes) { var config = base.GivenConfiguration(routes); config.GlobalConfiguration.RateLimitOptions = new() { ClientIdHeader = "ClientId", QuotaExceededMessage = "Exceeding!", RateLimitCounterPrefix = "ABC", HttpStatusCode = TooManyRequests, // 429 }; config.GlobalConfiguration.RequestIdKey = "OcelotClientRequest"; return config; } } ================================================ FILE: test/Ocelot.AcceptanceTests/RateLimiting/RateLimitingSteps.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.AcceptanceTests.RateLimiting; public class RateLimitingSteps : Steps { public Task WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) => WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(url, times); public async Task WhenIGetUrlOnTheApiGatewayMultipleTimesWithRateLimitingByAHeader(string url, int times, string clientIdHeader = "ClientId", string clientIdHeaderValue = "ocelotclient1") { List> tasks = new(); for (var i = 0; i < times; i++) { var request = new HttpRequestMessage(new(HttpMethods.Get), url); request.Headers.Add(clientIdHeader, clientIdHeaderValue); tasks.Add(ocelotClient.SendAsync(request)); } var responses = await Task.WhenAll(tasks); response = responses.Last(); return responses; } } ================================================ FILE: test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; namespace Ocelot.AcceptanceTests; public sealed class ReasonPhraseTests : Steps { public ReasonPhraseTests() { } [Fact] public void Should_return_reason_phrase() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", "some reason")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .And(_ => ThenTheResponseReasonPhraseIs("some reason")) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, string reasonPhrase) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { context.Response.HttpContext.Features.Get().ReasonPhrase = reasonPhrase; return context.Response.WriteAsync("YOYO!"); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Request/RequestMapperTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using System.Text; namespace Ocelot.AcceptanceTests.Request; [Trait("PR", "1972")] public sealed class RequestMapperTests : Steps { public RequestMapperTests() { } [Fact] public void Should_map_request_without_content() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(";;")) .BDDfy(); } [Fact] public void Should_map_request_with_content_length() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent("This is some content"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("20;;This is some content")) .BDDfy(); } [Fact] public void Should_map_request_with_empty_content() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StringContent(""))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("0;;")) .BDDfy(); } [Fact] public void Should_map_request_with_chunked_content() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new ChunkedContent("This ", "is some content"))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(";chunked;This is some content")) .BDDfy(); } [Fact] public void Should_map_request_with_empty_chunked_content() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new ChunkedContent())) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(";chunked;")) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode status) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { var request = context.Request; var response = context.Response; response.StatusCode = (int)status; await response.WriteAsync(request.ContentLength + ";" + request.Headers.TransferEncoding + ";"); await request.Body.CopyToAsync(response.Body); }); } private static FileRoute GivenRoute(int port, string method = null) => new() { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, DownstreamHostAndPorts = new() { new("localhost", port), }, UpstreamPathTemplate = "/", UpstreamHttpMethod = [method ?? HttpMethods.Get], }; } internal class ChunkedContent : HttpContent { private readonly string[] _chunks; public ChunkedContent(params string[] chunks) { _chunks = chunks; } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { foreach (var chunk in _chunks) { var bytes = Encoding.Default.GetBytes(chunk); await stream.WriteAsync(bytes, 0, bytes.Length); } } protected override bool TryComputeLength(out long length) { length = -1; return false; } } ================================================ FILE: test/Ocelot.AcceptanceTests/Request/StreamContentTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Ocelot.Configuration.File; using System.Security.Cryptography; namespace Ocelot.AcceptanceTests.Request; [Trait("PR", "1972")] // https://github.com/ThreeMammals/Ocelot/pull/1972 public sealed class StreamContentTests : Steps { #if NET10_0_OR_GREATER [Fact(Skip = "TODO Require fixing for net10.0 TFM or streaming feature review.")] #else [Fact] #endif public void Should_stream_with_content_length() { var contentSize = 1024L * 1024L * 1024L; // 1GB var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StreamTestContent(contentSize, false))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(contentSize + ";;" + contentSize)) .BDDfy(); } #if NET10_0_OR_GREATER [Fact(Skip = "TODO Require fixing for net10.0 TFM or streaming feature review.")] #else [Fact] #endif public async Task Should_stream_with_chunked_content() { var contentSize = 1024L * 1024L * 1024L; // 1GB var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningAsync()) .When(x => WhenIPostUrlOnTheApiGateway("/", new StreamTestContent(contentSize, true))) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(";chunked;" + contentSize)) .BDDfy(); } public override void GivenThereIsAServiceRunningOn(int port, string basePath) { static void options(KestrelServerOptions o) { o.Limits.MaxRequestBodySize = long.MaxValue; } var baseUrl = DownstreamUrl(port); handler.GivenThereIsAServiceRunningOnWithKestrelOptions(baseUrl, basePath, options, async context => { var request = context.Request; var response = context.Response; long streamLength = 0; int readBytes; var buffer = new byte[8192 - 1]; // Not aligned to sender do { readBytes = await request.Body.ReadAsync(buffer, 0, buffer.Length); streamLength += readBytes; } while (readBytes > 0); response.StatusCode = 200; await response.WriteAsync(request.ContentLength + ";" + request.Headers.TransferEncoding + ";" + streamLength); }); } private static FileRoute GivenRoute(int port, string method = null) => new() { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, DownstreamHostAndPorts = new() { new("localhost", port), }, UpstreamPathTemplate = "/", UpstreamHttpMethod = [method ?? HttpMethods.Get], }; } internal class StreamTestContent : HttpContent { private readonly long _size; private readonly bool _sendChunked; private readonly byte[] _dataBuffer; public StreamTestContent(long size, bool sendChunked) { _size = size; _sendChunked = sendChunked; _dataBuffer = RandomNumberGenerator.GetBytes(8192); } protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) { var remaining = _size; while (remaining > 0) { var count = (int)Math.Min(remaining, _dataBuffer.Length); await stream.WriteAsync(_dataBuffer, 0, count); remaining -= count; } } protected override bool TryComputeLength(out long length) { if (_sendChunked) { length = -1; return false; } else { length = _size; return true; } } } ================================================ FILE: test/Ocelot.AcceptanceTests/RequestIdTests.cs ================================================ namespace Ocelot.AcceptanceTests; public sealed class RequestIdTests : Steps { public const string RequestIdKey = "Oc-RequestId"; public RequestIdTests() { } [Fact] public void Should_use_default_request_id_and_forward() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.RequestIdKey = RequestIdKey; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheRequestIdIsReturned()) .BDDfy(); } [Fact] public void Should_use_request_id_and_forward() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); var requestId = Guid.NewGuid().ToString(); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayWithRequestId("/", requestId)) .Then(x => ThenTheRequestIdIsReturned(requestId)) .BDDfy(); } [Fact] public void Should_use_global_request_id_and_forward() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.RequestIdKey = RequestIdKey; var requestId = Guid.NewGuid().ToString(); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayWithRequestId("/", requestId)) .Then(x => ThenTheRequestIdIsReturned(requestId)) .BDDfy(); } [Fact] public void Should_use_global_request_id_create_and_forward() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.RequestIdKey = RequestIdKey; this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheRequestIdIsReturned()) .BDDfy(); } private async Task WhenIGetUrlOnTheApiGatewayWithRequestId(string url, string requestId) { ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); response = await ocelotClient.GetAsync(url); } private void GivenThereIsAServiceRunningOn(int port) { handler.GivenThereIsAServiceRunningOn(port, context => { context.Request.Headers.TryGetValue(RequestIdKey, out var requestId); context.Response.Headers[RequestIdKey] = requestId.First(); return Task.CompletedTask; }); } private void ThenTheRequestIdIsReturned() => response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); private void ThenTheRequestIdIsReturned(string expected) => response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); } ================================================ FILE: test/Ocelot.AcceptanceTests/Requester/MessageInvokerPoolTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester; namespace Ocelot.AcceptanceTests.Requester; public sealed class MessageInvokerPoolTests : RequesterSteps { #region TODO Redevelop to minimize the code [Fact] [Trait("PR", "1824")] // https://github.com/ThreeMammals/Ocelot/pull/1824 public async Task Should_reuse_cookies_from_container() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(new()) .WithHttpHandlerOptions(new() { UseCookieContainer = true, UseProxy = true }) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) // The test should pass without timeout definition -> implicit default timeout //.WithTimeout(DownstreamRoute.DefaultTimeoutSeconds) .Build(); //using ServiceHandler handler = new(); var port = PortFinder.GetRandomPort(); GivenADownstreamService(port); // sometimes it fails because of port binding GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); GivenARequest(route, port); // Act, Assert var toUrl = DownstreamUrl(port); await WhenICallTheClient(toUrl); _response.Headers.TryGetValues("Set-Cookie", out _).ShouldBeTrue(); // Act, Assert await WhenICallTheClient(toUrl); _response.StatusCode.ShouldBe(HttpStatusCode.OK); } private Mock _handlerFactory; private HttpResponseMessage _response; private MessageInvokerPool _pool; private readonly DefaultHttpContext _context = new(); private readonly Mock _ocelotLogger = new(); private readonly Mock _ocelotLoggerFactory = new(); private async Task WhenICallTheClient(string url) { var messageInvoker = _pool.Get(_context.Items.DownstreamRoute()); _response = await messageInvoker .SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None); } private void GivenAMessageInvokerPool() => _pool = new MessageInvokerPool(_handlerFactory.Object, _ocelotLoggerFactory.Object); private void GivenTheFactoryReturns(List handlers) { _handlerFactory = new Mock(); _handlerFactory.Setup(x => x.Get(It.IsAny())) .Returns(handlers); } private void GivenARequest(DownstreamRoute downstream, int port) => GivenARequestWithAUrlAndMethod(downstream, port, HttpMethod.Get); private void GivenARequestWithAUrlAndMethod(DownstreamRoute downstream, int port, HttpMethod method) { var url = DownstreamUrl(port); _context.Items.UpsertDownstreamRoute(downstream); _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri(url), Method = method })); } private void GivenADownstreamService(int port) { var count = 0; handler.GivenThereIsAServiceRunningOn(port, context => { if (count == 0) { context.Response.Cookies.Append("test", "0"); context.Response.StatusCode = 200; count++; return Task.CompletedTask; } if (count == 1) { if (context.Request.Cookies.TryGetValue("test", out var cookieValue) || context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) { context.Response.StatusCode = 200; return Task.CompletedTask; } context.Response.StatusCode = 500; } return Task.CompletedTask; }); } #endregion [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public async Task ShouldApplyGlobalHttpHandlerOptions_ForStaticRoutes() { var ports = PortFinder.GetPorts(3); var route1 = GivenRoute(ports[0], "/route1", null); // no opts -> use global opts var route2 = GivenRoute(ports[1], "/route2", GivenOptions(99, 99, useTracing: true)); var route3 = GivenRoute(ports[2], "/noTracing", GivenOptions()); var configuration = GivenConfiguration(route1, route2, route3); // static routes come to Routes collection var globalOptions = configuration.GlobalConfiguration.HttpHandlerOptions = new(GivenOptions(100, 100, useTracing: false)); GivenThereIsAServiceRunningOnPath(ports[0], "/route1"); GivenThereIsAServiceRunningOnPath(ports[1], "/route2"); GivenThereIsAServiceRunningOnPath(ports[2], "/noTracing"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithRequesterTesting); await WhenIGetUrlOnTheApiGateway("/route1"); await WhenIGetUrlOnTheApiGateway("/route2"); await WhenIGetUrlOnTheApiGateway("/noTracing"); ThenTheResponseBody(); ThenRouteHttpHandlerOptionsAre(route1, globalOptions.MaxConnectionsPerServer.Value, globalOptions.PooledConnectionLifetimeSeconds.Value, globalOptions.UseTracing.Value); ThenRouteHttpHandlerOptionsAre(route2, 99, 99, true); ThenRouteHttpHandlerOptionsAre(route3, 100, 100, false); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public async Task ShouldApplyGlobalGroupHttpHandlerOptions_ForStaticRoutes_WhenRouteOptsHasAKey() { // 1st route var ports = PortFinder.GetPorts(3); var route1 = GivenRoute(ports[0], "/route1", null); route1.Key = null; // 1st route is not in the global group // 2nd route var route2 = GivenRoute(ports[1], "/route2", null); // 2nd route opts will be applied from global ones route2.Key = "R2"; // 2nd route is in the group // 3rd route var route3 = GivenRoute(ports[2], "/noTracing", GivenOptions(88, 88, useTracing: false)); var configuration = GivenConfiguration(route1, route2, route3); var globalOptions = configuration.GlobalConfiguration.HttpHandlerOptions = new(GivenOptions(100, 100, useTracing: true)) { RouteKeys = ["R2"], }; GivenThereIsAServiceRunningOnPath(ports[0], "/route1"); GivenThereIsAServiceRunningOnPath(ports[1], "/route2"); GivenThereIsAServiceRunningOnPath(ports[2], "/noTracing"); GivenThereIsAConfiguration(configuration); await GivenOcelotIsRunningAsync(WithRequesterTesting); await WhenIGetUrlOnTheApiGateway("/route1"); await WhenIGetUrlOnTheApiGateway("/route2"); await WhenIGetUrlOnTheApiGateway("/noTracing"); ThenTheResponseBody(); ThenRouteHttpHandlerOptionsAre(route1, int.MaxValue, HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, false); ThenRouteHttpHandlerOptionsAre(route2, globalOptions.MaxConnectionsPerServer.Value, globalOptions.PooledConnectionLifetimeSeconds.Value, globalOptions.UseTracing.Value); ThenRouteHttpHandlerOptionsAre(route3, 88, 88, false); } private void ThenRouteHttpHandlerOptionsAre(FileRoute route, int maxConnections, int seconds, bool useTracing) { var pool = OcelotServices.GetService() as TestMessageInvokerPool; var tracer = OcelotServices.GetService() as TestTracer; var kv = pool.ShouldNotBeNull() .CreatedHandlers.Single(x => x.Key.UpstreamPathTemplate.OriginalValue == route.UpstreamPathTemplate); var downstream = kv.Key; var httpHandler = kv.Value; httpHandler.MaxConnectionsPerServer.ShouldBe(maxConnections); httpHandler.PooledConnectionLifetime.TotalSeconds.ShouldBe(seconds); downstream.HttpHandlerOptions.UseTracing.ShouldBe(useTracing); var request = tracer.Requests.Keys.SingleOrDefault(k => k.RequestUri.AbsolutePath == route.UpstreamPathTemplate); (request != null).ShouldBe(useTracing); } private static FileHttpHandlerOptions GivenOptions(int maxConnections = 100, int pooledConnectionSeconds = 100, bool useCookieContainer = false, bool useProxy = false, bool useTracing = false) => new() { MaxConnectionsPerServer = maxConnections, PooledConnectionLifetimeSeconds = pooledConnectionSeconds, UseCookieContainer = useCookieContainer, UseProxy = useProxy, UseTracing = useTracing, }; private FileRoute GivenRoute(int port, string path = null, FileHttpHandlerOptions options = null) { var r = GivenRoute(port, path, path); r.HttpHandlerOptions = options; return r; } } ================================================ FILE: test/Ocelot.AcceptanceTests/Requester/PayloadTooLargeTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using System.Runtime.InteropServices; using System.Text; namespace Ocelot.AcceptanceTests.Requester; [Trait("Bug", "749")] // https://github.com/ThreeMammals/Ocelot/issues/749 [Trait("PR", "1769")] // https://github.com/ThreeMammals/Ocelot/pull/1769 public sealed class PayloadTooLargeTests : Steps { private const string Payload = "[{\"_id\":\"6540f8ee7beff536c1304e3a\",\"index\":0,\"guid\":\"349307e2-5b1b-4ea9-8e42-d0d26b35059e\",\"isActive\":true,\"balance\":\"$2,458.86\",\"picture\":\"http://placehold.it/32x32\",\"age\":36,\"eyeColor\":\"blue\",\"name\":\"WalshSloan\",\"gender\":\"male\",\"company\":\"ENOMEN\",\"email\":\"walshsloan@enomen.com\",\"phone\":\"+1(818)463-2479\",\"address\":\"863StoneAvenue,Islandia,NewHampshire,7062\",\"about\":\"Exvelitelitutsintlaborisofficialaborisreprehenderittemporsitminim.Exveniamexetesse.Reprehenderitirurealiquipsuntnostrudcillumaliquipsuntvoluptateessenisivoluptatetemporexercitationsint.Laborumexestipsumincididuntvelit.Idnisiproidenttemporelitnonconsequatestnostrudmollit.\\r\\n\",\"registered\":\"2014-11-13T01:53:09-01:00\",\"latitude\":-1.01137,\"longitude\":160.133312,\"tags\":[\"nisi\",\"eu\",\"anim\",\"ipsum\",\"fugiat\",\"excepteur\",\"culpa\"],\"friends\":[{\"id\":0,\"name\":\"MayNoel\"},{\"id\":1,\"name\":\"RichardsDiaz\"},{\"id\":2,\"name\":\"JannieHarvey\"}],\"greeting\":\"Hello,WalshSloan!Youhave6unreadmessages.\",\"favoriteFruit\":\"banana\"},{\"_id\":\"6540f8ee39e04d0ac854b05d\",\"index\":1,\"guid\":\"0f210e11-94a1-45c7-84a4-c2bfcbe0bbfb\",\"isActive\":false,\"balance\":\"$3,371.91\",\"picture\":\"http://placehold.it/32x32\",\"age\":25,\"eyeColor\":\"green\",\"name\":\"FergusonIngram\",\"gender\":\"male\",\"company\":\"DOGSPA\",\"email\":\"fergusoningram@dogspa.com\",\"phone\":\"+1(804)599-2376\",\"address\":\"130RiverStreet,Bellamy,DistrictOfColumbia,9522\",\"about\":\"Duisvoluptatemollitullamcomollitessedolorvelit.Nonpariaturadipisicingsintdoloranimveniammollitdolorlaborumquisnulla.Ametametametnonlaborevoluptate.Eiusmoddocupidatatveniamirureessequiullamcoincididuntea.\\r\\n\",\"registered\":\"2014-11-01T03:51:36-01:00\",\"latitude\":-57.122954,\"longitude\":-91.22665,\"tags\":[\"nostrud\",\"ipsum\",\"id\",\"cupidatat\",\"consectetur\",\"labore\",\"ullamco\"],\"friends\":[{\"id\":0,\"name\":\"TabithaHuffman\"},{\"id\":1,\"name\":\"LydiaStark\"},{\"id\":2,\"name\":\"FaithStuart\"}],\"greeting\":\"Hello,FergusonIngram!Youhave3unreadmessages.\",\"favoriteFruit\":\"banana\"}]"; [Fact] public void Should_throw_payload_too_large_exception_using_kestrel() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(1024)) .When(x => WhenIPostUrlOnTheApiGateway("/", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload)))) .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge)) .BDDfy(); } [Fact] public void Should_throw_payload_too_large_exception_using_http_sys() { Assert.SkipUnless(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "Test is unstable for all platforms except Windows OS"); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, HttpMethods.Post); var configuration = GivenConfiguration(route); GivenThereIsAServiceRunningOn(port); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(1024)) .When(x => WhenIPostUrlOnTheApiGateway("/", new ByteArrayContent(Encoding.UTF8.GetBytes(Payload)))) .Then(x => ThenTheStatusCodeShouldBe((int)HttpStatusCode.RequestEntityTooLarge)) .BDDfy(); } private static FileRoute GivenRoute(int port, string method = null) => new() { DownstreamPathTemplate = "/", DownstreamHostAndPorts = new() { new("localhost", port), }, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", UpstreamHttpMethod = [method ?? HttpMethods.Get], }; private Task GivenOcelotIsRunningOnKestrelWithCustomBodyMaxSize(long customBodyMaxSize) => GivenOcelotHostIsRunning( null, null, null, null, host => host.UseKestrel(options => options.Limits.MaxRequestBodySize = customBodyMaxSize), null, null); #pragma warning disable IDE0079 // Remove unnecessary suppression #pragma warning disable CA1416 // Validate platform compatibility private Task GivenOcelotIsRunningOnHttpSysWithCustomBodyMaxSize(long customBodyMaxSize) { int port = PortFinder.GetRandomPort(); var ocelotUrl = DownstreamUrl(port); void ConfigureHttpSys(IWebHostBuilder builder) => builder .ConfigureAppConfiguration(WithBasicConfiguration) .ConfigureServices(WithAddOcelot) .Configure(WithUseOcelot) .UseUrls(ocelotUrl) .UseHttpSys(options => options.MaxRequestBodySize = customBodyMaxSize); return GivenOcelotHostIsRunning(null, null, null, ConfigureHttpSys, null, null, client => client.BaseAddress = new(ocelotUrl)); } #pragma warning restore CA1416 #pragma warning restore IDE0079 } ================================================ FILE: test/Ocelot.AcceptanceTests/Requester/RequesterSteps.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.DependencyInjection; using Ocelot.Logging; using Ocelot.Requester; namespace Ocelot.AcceptanceTests.Requester; public class RequesterSteps : Steps { public static void WithRequesterTesting(IServiceCollection services) => WithRequesterTesting(services, true); public static void WithRequesterTesting(IServiceCollection services, bool addOcelot) { if (addOcelot) services.AddOcelot(); services .AddSingleton() .RemoveAll() .AddSingleton(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Requester/TestMessageInvokerPool.cs ================================================ using Ocelot.Configuration; using Ocelot.Logging; using Ocelot.Requester; using System.Collections.Concurrent; namespace Ocelot.AcceptanceTests.Requester; public class TestMessageInvokerPool : MessageInvokerPool, IMessageInvokerPool { public TestMessageInvokerPool(IDelegatingHandlerFactory handlerFactory, IOcelotLoggerFactory loggerFactory) : base(handlerFactory, loggerFactory) { } public readonly ConcurrentDictionary CreatedHandlers = new(); protected override SocketsHttpHandler CreateHandler(DownstreamRoute route) { var handler = base.CreateHandler(route); CreatedHandlers[route] = handler; return handler; } } ================================================ FILE: test/Ocelot.AcceptanceTests/Requester/TestTracer.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Logging; using System.Collections.Concurrent; namespace Ocelot.AcceptanceTests.Requester; public class TestTracer : IOcelotTracer { public readonly ConcurrentBag Events = new(); public readonly ConcurrentDictionary Requests = new(); public void Event(HttpContext httpContext, string @event) => Events.Add(@event); public async Task SendAsync(HttpRequestMessage request, Action addTraceIdToRepo, Func> baseSendAsync, CancellationToken cancellationToken) { addTraceIdToRepo?.Invoke("12345"); var response = await baseSendAsync.Invoke(request, cancellationToken).ConfigureAwait(false); Requests[request] = response; return response; } } ================================================ FILE: test/Ocelot.AcceptanceTests/ResponseCodeTests.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.AcceptanceTests; public sealed class ResponseCodeTests : Steps { public ResponseCodeTests() { } [Fact] public void ShouldReturnResponse304WhenServiceReturns304() { var port = PortFinder.GetRandomPort(); var route = GivenCatchAllRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/inline.132.bundle.js", HttpStatusCode.NotModified)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/inline.132.bundle.js")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotModified)) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(statusCode.ToString()); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Logging; namespace Ocelot.AcceptanceTests; [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea public sealed class ReturnsErrorTests : Steps { [Fact] [Trait("Feat", "603")] // https://github.com/ThreeMammals/Ocelot/issues/603 [Trait("PR", "1149")] // https://github.com/ThreeMammals/Ocelot/pull/1149 [Trait("Release", "15.0.1")] // https://github.com/ThreeMammals/Ocelot/releases/tag/15.0.1 public void Should_return_bad_gateway_error_if_downstream_service_doesnt_respond() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } [Fact] [Trait("Commit", "1599694")] // https://github.com/ThreeMammals/Ocelot/commit/159969483b64c5491b1d86b1aa4dac7b4b2a3ba1 [Trait("Commit", "ef3deec")] // https://github.com/ThreeMammals/Ocelot/commit/ef3deec8da78fd282f6b5f2bff8e6d6853496c31 [Trait("Release", "1.4.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.0 public void Should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.InternalServerError)) .BDDfy(); } [Fact] [Trait("Feat", "492")] // https://github.com/ThreeMammals/Ocelot/issues/492 [Trait("PR", "1055")] // https://github.com/ThreeMammals/Ocelot/pull/1055 [Trait("Release", "14.0.4")] // https://github.com/ThreeMammals/Ocelot/releases/tag/14.0.4 [Trait("Commit", "9da55ea")] // https://github.com/ThreeMammals/Ocelot/commit/9da55ea037d0df3b8b22d32dec9b004a50709251 [Trait("PR", "1106")] // https://github.com/ThreeMammals/Ocelot/pull/1106 public void Should_log_warning_if_downstream_service_returns_internal_server_error() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningWithLogger()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenWarningShouldBeLogged(1)) .BDDfy(); } private Task GivenOcelotIsRunningWithLogger() => GivenOcelotIsRunningAsync(s => s .AddOcelot() .Services .AddSingleton()); private void GivenThereIsAServiceRunningOn(int port) { handler.GivenThereIsAServiceRunningOn(port, context => throw new Exception("BLAMMMM")); } private void ThenWarningShouldBeLogged(int howMany) { var loggerFactory = (MockLoggerFactory)OcelotServices.GetService(); loggerFactory.Verify(Times.Exactly(howMany)); } internal class MockLoggerFactory : IOcelotLoggerFactory { private Mock _logger; private bool _disposed; public IOcelotLogger CreateLogger() { if (_disposed) return null; if (_logger != null) return _logger.Object; _logger = new Mock(); _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); return _logger.Object; } public void Verify(Times howMany) => _logger.Verify(x => x.LogWarning(It.IsAny>()), howMany); protected virtual void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _logger = null; } _logger = null; _disposed = true; } public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } } } ================================================ FILE: test/Ocelot.AcceptanceTests/Routing/RoutingBasedOnHeadersTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests.Routing; [Trait("PR", "1312")] [Trait("Feat", "360")] public sealed class RoutingBasedOnHeadersTests : Steps { private string _downstreamPath; public RoutingBasedOnHeadersTests() { } [Fact] public void Should_match_one_header_value() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, headerValue)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } [Fact] public void Should_match_one_header_value_when_more_headers() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader("other", "otherValue")) .And(x => GivenIAddAHeader(headerName, headerValue)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } [Fact] public void Should_match_two_header_values_when_more_headers() { var port = PortFinder.GetRandomPort(); var headerName1 = "country_code"; var headerValue1 = "PL"; var headerName2 = "region"; var headerValue2 = "MAZ"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName1] = headerValue1, [headerName2] = headerValue2, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName1, headerValue1)) .And(x => GivenIAddAHeader("other", "otherValue")) .And(x => GivenIAddAHeader(headerName2, headerValue2)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } [Fact] public void Should_not_match_one_header_value() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var anotherHeaderValue = "UK"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, anotherHeaderValue)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_not_match_one_header_value_when_no_headers() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_not_match_two_header_values_when_one_different() { var port = PortFinder.GetRandomPort(); var headerName1 = "country_code"; var headerValue1 = "PL"; var headerName2 = "region"; var headerValue2 = "MAZ"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName1] = headerValue1, [headerName2] = headerValue2, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName1, headerValue1)) .And(x => GivenIAddAHeader("other", "otherValue")) .And(x => GivenIAddAHeader(headerName2, "anothervalue")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_not_match_two_header_values_when_one_not_existing() { var port = PortFinder.GetRandomPort(); var headerName1 = "country_code"; var headerValue1 = "PL"; var headerName2 = "region"; var headerValue2 = "MAZ"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName1] = headerValue1, [headerName2] = headerValue2, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName1, headerValue1)) .And(x => GivenIAddAHeader("other", "otherValue")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_not_match_one_header_value_when_header_duplicated() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, headerValue)) .And(x => GivenIAddAHeader(headerName, "othervalue")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_aggregated_route_match_header_value() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var routeA = GivenRouteWithKey(port1, "/a", "Laura"); var routeB = GivenRouteWithKey(port2, "/b", "Tom"); var route = GivenAggRouteWithUpstreamHeaderTemplates(new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(routeA, routeB); configuration.Aggregates.Add(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) .And(x => GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, headerValue)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_aggregated_route_not_match_header_value() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue = "PL"; var routeA = GivenRouteWithKey(port1, "/a", "Laura"); var routeB = GivenRouteWithKey(port2, "/b", "Tom"); var route = GivenAggRouteWithUpstreamHeaderTemplates(new() { [headerName] = headerValue, }); var configuration = GivenConfiguration(routeA, routeB); configuration.Aggregates.Add(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/a", HttpStatusCode.OK, Hello("Laura"))) .And(x => x.GivenThereIsAServiceRunningOn(port2, "/b", HttpStatusCode.OK, Hello("Tom"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_match_header_placeholder() { var port = PortFinder.GetRandomPort(); var headerName = "Region"; var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/api.internal-{code}/products", new() { [headerName] = "{header:code}", }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api.internal-uk/products", HttpStatusCode.OK, Hello("UK"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, "uk")) .When(x => WhenIGetUrlOnTheApiGateway("/products")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello("UK"))) .BDDfy(); } [Fact] public void Should_match_header_placeholder_not_in_downstream_path() { var port = PortFinder.GetRandomPort(); var headerName = "ProductName"; var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-info", new() { [headerName] = "product-{header:everything}", }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-info", HttpStatusCode.OK, Hello("products"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, "product-Camera")) .When(x => WhenIGetUrlOnTheApiGateway("/products")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello("products"))) .BDDfy(); } [Fact] public void Should_distinguish_route_for_different_roles() { var port = PortFinder.GetRandomPort(); var headerName = "Origin"; var route = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products-admin", new() { [headerName] = "admin.xxx.com", }); var route2 = GivenRouteWithUpstreamHeaderTemplates(port, "/products", "/products", null); var configuration = GivenConfiguration(route, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products-admin", HttpStatusCode.OK, Hello("products admin"))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, "admin.xxx.com")) .When(x => WhenIGetUrlOnTheApiGateway("/products")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello("products admin"))) .BDDfy(); } [Fact] public void Should_match_header_and_url_placeholders() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var route = GivenRouteWithUpstreamHeaderTemplates(port, "/{aa}", "/{country_code}/{version}/{aa}", new() { [headerName] = "start_{header:country_code}_version_{header:version}_end", }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/pl/v1/bb", HttpStatusCode.OK, Hello())) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, "start_pl_version_v1_end")) .When(x => WhenIGetUrlOnTheApiGateway("/bb")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } [Fact] public void Should_match_header_with_braces() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var route = GivenRouteWithUpstreamHeaderTemplates(port, "/", "/aa", new() { [headerName] = "my_{header}", }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/aa", HttpStatusCode.OK, Hello())) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, "my_{header}")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } [Fact] public void Should_match_two_headers_with_the_same_name() { var port = PortFinder.GetRandomPort(); var headerName = "country_code"; var headerValue1 = "PL"; var headerValue2 = "UK"; var multipleValues = new StringValues([headerValue1, "{header:whatever}"]); var route = GivenRouteWithUpstreamHeaderTemplates(port, new() { [headerName] = multipleValues.ToString(), // headerValue1 + ";{header:whatever}", }); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader(headerName, headerValue1)) .And(x => GivenIAddAHeader(headerName, headerValue2)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(Hello())) .BDDfy(); } private static string Hello() => Hello("Jolanta"); private static string Hello(string who) => $"Hello from {who}"; private void GivenThereIsAServiceRunningOn(int port) => GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, Hello()); private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { basePath ??= "/"; responseBody ??= Hello(); handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (_downstreamPath != basePath) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync($"{nameof(_downstreamPath)} is not equal to {nameof(basePath)}"); } else { context.Response.StatusCode = (int)statusCode; await context.Response.WriteAsync(responseBody); } }); } private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected); private FileRoute GivenRouteWithKey(int port, string path = null, string key = null) => GivenRoute(port, path, path).WithKey(key); private FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, Dictionary templates) { var route = GivenDefaultRoute(port); route.UpstreamHeaderTemplates = templates; return route; } private FileRoute GivenRouteWithUpstreamHeaderTemplates(int port, string upstream, string downstream, Dictionary templates) { var route = GivenRoute(port, upstream, downstream); route.UpstreamHeaderTemplates = templates; return route; } private static FileAggregateRoute GivenAggRouteWithUpstreamHeaderTemplates(Dictionary templates) => new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Laura", "Tom"], UpstreamHeaderTemplates = templates, }; } ================================================ FILE: test/Ocelot.AcceptanceTests/Routing/RoutingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Balancers; using System.Web; namespace Ocelot.AcceptanceTests.Routing; public sealed class RoutingTests : Steps { private string _downstreamPath; private string _downstreamQuery; public RoutingTests() { } [Fact] public void Should_not_match_forward_slash_in_pattern_before_next_forward_slash() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/v{apiVersion}/cards", "/api/v{apiVersion}/cards") .WithPriority(1); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/aaaaaaaaa/cards", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/api/v1/aaaaaaaaa/cards")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_return_response_404_when_no_configuration_at_all() { this.Given(x => GivenThereIsAConfiguration(new())) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] public void Should_return_response_200_with_forward_slash_and_placeholder_only() { var port = PortFinder.GetRandomPort(); var route = GivenCatchAllRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_favouring_forward_slash_with_path_route() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenCatchAllRoute(port1); var route2 = GivenDefaultRoute(port2); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/test", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/test")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_favouring_forward_slash() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenCatchAllRoute(port1); var route2 = GivenDefaultRoute(port2); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port2, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_favouring_forward_slash_route_because_it_is_first() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenDefaultRoute(port1); var route2 = GivenCatchAllRoute(port2); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port1, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_with_nothing_and_placeholder_only() { var port = PortFinder.GetRandomPort(); var route = GivenCatchAllRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(string.Empty)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "134")] public void Should_fix_issue_134() { var port = PortFinder.GetRandomPort(); //var port2 = PortFinder.GetRandomPort(); var methods = new string[] { HttpMethods.Options, HttpMethods.Put, HttpMethods.Get, HttpMethods.Post, HttpMethods.Delete }; var route1 = GivenRoute(port, "/vacancy/", "/api/v1/vacancy") .WithMethods(methods); var route2 = GivenRoute(port, "/vacancy/{vacancyId}", "/api/v1/vacancy/{vacancyId}") .WithMethods(methods); route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection)); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/vacancy/1", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/vacancy/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_when_path_missing_forward_slash_as_first_char() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/", "/api/products"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_when_host_has_trailing_slash() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/", "/api/products"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Theory] [InlineData("/products")] [InlineData("/products/")] public void Should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not(string url) { const string downstreamBasePath = "/products"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, $"{downstreamBasePath}/", downstreamBasePath); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamBasePath, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(url)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Theory] [Trait("Bug", "649")] [InlineData("/account/authenticate")] [InlineData("/account/authenticate/")] public void Should_fix_issue_649(string url) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/account/authenticate/", "/authenticate"); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.BaseUrl = DownstreamUrl(port); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/authenticate", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(url)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/products", "/products"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/products", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/products/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Theory] [Trait("Bug", "683")] [InlineData("/products/{productId}", "/products/{productId}", "/products/")] public void Should_return_response_200_with_empty_placeholder(string downstream, string upstream, string requestURL) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, requestURL, HttpStatusCode.OK, "Hello from Aly")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestURL)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Aly")) .BDDfy(); } [Fact] public void Should_return_response_200_with_complex_url() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/products/{productId}", "/api/products/{productId}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/products/1", HttpStatusCode.OK, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/products/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Some Product")) .BDDfy(); } [Fact] public void Should_return_response_200_with_complex_url_that_starts_with_placeholder() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/{variantId}/products/{productId}", "/api/{variantId}/products/{productId}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/23/products/1", HttpStatusCode.OK, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("23/products/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Some Product")) .BDDfy(); } [Fact] public void Should_not_add_trailing_slash_to_downstream_url() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/products/{productId}", "/api/products/{productId}"); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, "/api/products/1", HttpStatusCode.OK, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/products/1")) .Then(x => ThenTheDownstreamUrlPathShouldBe("/api/products/1")) .BDDfy(); } [Fact] public void Should_return_response_201_with_simple_url() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.Created, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", "postContent")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) .BDDfy(); } [Fact] public void Should_return_response_201_with_complex_query_string() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/newThing", "/newThing"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/newThing", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/newThing?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Theory] [Trait("Feat", "89")] [InlineData("/api/{finalUrlPath}", "/api/api1/{finalUrlPath}", "/api/api1/product/products/categories/", "/api/product/products/categories/")] [InlineData("/api/{urlPath}", "/myApp1Name/api/{urlPath}", "/myApp1Name/api/products/1", "/api/products/1")] public void Should_return_response_200_with_placeholder_for_final_url_path2(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL, string downstreamPath) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstreamPathTemplate, downstreamPathTemplate); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Some Product")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestURL)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Some Product")) .BDDfy(); } [Theory] [Trait("Bug", "748")] // https://github.com/ThreeMammals/Ocelot/issues/748 [InlineData("/downstream/test/{everything}", "/upstream/test/{everything}", "/upstream/test/1", "/downstream/test/1", "?p1=v1&p2=v2&something-else=")] [InlineData("/downstream/test/{everything}", "/upstream/test/{everything}", "/upstream/test/", "/downstream/test/", "?p1=v1&p2=v2&something-else=")] [InlineData("/downstream/test/{everything}", "/upstream/test/{everything}", "/upstream/test", "/downstream/test", "?p1=v1&p2=v2&something-else=")] [InlineData("/downstream/test/{everything}", "/upstream/test/{everything}", "/upstream/test123", null, null)] [InlineData("/downstream/{version}/test/{everything}", "/upstream/{version}/test/{everything}", "/upstream/v1/test/123", "/downstream/v1/test/123", "?p1=v1&p2=v2&something-else=")] [InlineData("/downstream/{version}/test", "/upstream/{version}/test", "/upstream/v1/test", "/downstream/v1/test", "?p1=v1&p2=v2&something-else=")] [InlineData("/downstream/{version}/test", "/upstream/{version}/test", "/upstream/test", null, null)] public void Should_return_correct_downstream_when_omitting_ending_placeholder(string downstreamPathTemplate, string upstreamPathTemplate, string requestURL, string downstreamURL, string queryString) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstreamPathTemplate, downstreamPathTemplate); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Aly")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestURL)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamURL)) // Now check the same URL but with query string // Catch-All placeholder should forward any path + query string combinations to the downstream service // More: https://ocelot.readthedocs.io/en/latest/features/routing.html#placeholders:~:text=This%20will%20forward%20any%20path%20%2B%20query%20string%20combinations%20to%20the%20downstream%20service%20after%20the%20path%20%2Fapi. .When(x => WhenIGetUrlOnTheApiGateway(requestURL + queryString)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamURL)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString)) .BDDfy(); } [Trait("PR", "1911")] [Trait("Link", "https://ocelot.readthedocs.io/en/latest/features/routing.html#catch-all-query-string")] [Theory(DisplayName = "Catch All Query String should be forwarded with all query string parameters with(out) last slash")] [InlineData("/apipath/contracts?{everything}", "/contracts?{everything}", "/contracts", "/apipath/contracts", "")] [InlineData("/apipath/contracts?{everything}", "/contracts?{everything}", "/contracts?", "/apipath/contracts", "")] [InlineData("/apipath/contracts?{everything}", "/contracts?{everything}", "/contracts?p1=v1&p2=v2", "/apipath/contracts", "?p1=v1&p2=v2")] [InlineData("/apipath/contracts/?{everything}", "/contracts/?{everything}", "/contracts/?", "/apipath/contracts/", "")] [InlineData("/apipath/contracts/?{everything}", "/contracts/?{everything}", "/contracts/?p3=v3&p4=v4", "/apipath/contracts/", "?p3=v3&p4=v4")] [InlineData("/apipath/contracts?{everything}", "/contracts?{everything}", "/contracts?filter=(-something+123+else)", "/apipath/contracts", "?filter=(-something%20123%20else)")] public void Should_forward_Catch_All_query_string_when_last_slash(string downstream, string upstream, string requestURL, string downstreamPath, string queryString) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Hello from Raman")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestURL)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath)) // ! .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString)) // !! .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Raman")) .BDDfy(); } [Theory] [Trait("Bug", "2199")] [Trait("Feat", "2200")] [InlineData("/api/invoices/{url0}-{url1}-{url2}", "/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}", "/api/invoices_abc/def-ghi_abcd/xyz?urlId=bla", "/api/invoices/abc-def-ghi", "?urlId=bla")] [InlineData("/api/products/{category}-{subcategory}/{filter}", "/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}", "/api/products_electronics/computers_details/123?filter=active", "/api/products/electronics-computers/active", "")] [InlineData("/api/users/{userId}/posts/{postId}/{lang}", "/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}", "/api/users/101/2022_content/2024?lang=en", "/api/users/101/posts/2022/en", "")] [InlineData("/api/categories/{cat}-{subcat}?sort={sort}", "/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}", "/api/categories_home/furniture_items/789?sort=asc", "/api/categories/home-furniture", "?sort=asc")] [InlineData("/api/orders/{order}-{detail}?status={status}", "/api/orders_{order}/{detail}_invoice/{ref}?status={status}", "/api/orders_987/abc_invoice/123?status=shipped", "/api/orders/987-abc", "?status=shipped")] [InlineData("/api/transactions/{type}-{region}", "/api/transactions_{type}/{region}_summary/{year}?q={query}", "/api/transactions_sales/NA_summary/2024?q=forecast", "/api/transactions/sales-NA", "?q=forecast")] [InlineData("/api/resources/{resource}-{subresource}", "/api/resources_{resource}/{subresource}_data/{id}?key={apikey}", "/api/resources_images/photos_data/555?key=xyz123", "/api/resources/images-photos", "?key=xyz123")] [InlineData("/api/accounts/{account}-{detail}", "/api/accounts_{account}/{detail}_info/{id}?opt={option}", "/api/accounts_admin/settings_info/101?opt=true", "/api/accounts/admin-settings", "?opt=true")] public void ShouldMatchComplexQueriesWithEmbeddedPlaceholders(string downstream, string upstream, string requestUrl, string downstreamPath, string queryString) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Hello from Guillaume")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestUrl)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Guillaume")) .BDDfy(); } [Theory] [Trait("Bug", "2209")] [InlineData("/api/invoices/{url0}-{url1}-{url2}", "/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}", "/api/InvoIces_abc/def-ghi_abcd/xyz?urlId=bla", "/api/invoices/abc-def-ghi", "?urlId=bla")] [InlineData("/api/products/{category}-{subcategory}/{filter}", "/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}", "/API/PRODUCTS_electronics/computers_details/123?filter=active", "/api/products/electronics-computers/active", "")] [InlineData("/api/users/{userId}/posts/{postId}/{lang}", "/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}", "/api/UsErS/101/2022_content/2024?lang=en", "/api/users/101/posts/2022/en", "")] [InlineData("/api/categories/{cat}-{subcat}?sort={sort}", "/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}", "/api/CATEGORIES_home/furniture_items/789?sort=asc", "/api/categories/home-furniture", "?sort=asc")] [InlineData("/api/orders/{order}-{detail}?status={status}", "/api/orders_{order}/{detail}_invoice/{ref}?status={status}", "/API/ORDERS_987/abc_invOiCE/123?status=shipped", "/api/orders/987-abc", "?status=shipped")] [InlineData("/api/transactions/{type}-{region}", "/api/transactions_{type}/{region}_summary/{year}?q={query}", "/api/TRanSacTIONS_sales/NA_summary/2024?q=forecast", "/api/transactions/sales-NA", "?q=forecast")] public void ShouldMatchComplexQueriesCaseInsensitive(string downstream, string upstream, string requestUrl, string downstreamPath, string queryString) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Hello from Guillaume")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestUrl)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(queryString)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Guillaume")) .BDDfy(); } [Theory] [Trait("Bug", "2209")] [InlineData("/api/invoices/{url0}-{url1}-{url2}", "/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}", "/api/InvoIces_abc/def-ghi_abcd/xyz?urlId=bla", "/api/invoices/abc-def-ghi")] [InlineData("/api/products/{category}-{subcategory}/{filter}", "/api/products_{category}/{subcategory}_details/{itemId}?filter={filter}", "/API/PRODUCTS_electronics/computers_details/123?filter=active", "/api/products/electronics-computers/active")] [InlineData("/api/users/{userId}/posts/{postId}/{lang}", "/api/users/{userId}/{postId}_content/{timestamp}?lang={lang}", "/api/UsErS/101/2022_content/2024?lang=en", "/api/users/101/posts/2022/en")] [InlineData("/api/categories/{cat}-{subcat}?sort={sort}", "/api/categories_{cat}/{subcat}_items/{itemId}?sort={sort}", "/api/CATEGORIES_home/furniture_items/789?sort=asc", "/api/categories/home-furniture")] [InlineData("/api/orders/{order}-{detail}?status={status}", "/api/orders_{order}/{detail}_invoice/{ref}?status={status}", "/API/ORDERS_987/abc_invOiCE/123?status=shipped", "/api/orders/987-abc")] [InlineData("/api/transactions/{type}-{region}", "/api/transactions_{type}/{region}_summary/{year}?q={query}", "/api/TRanSacTIONS_sales/NA_summary/2024?q=forecast", "/api/transactions/sales-NA")] public void ShouldNotMatchComplexQueriesCaseSensitive(string downstream, string upstream, string requestUrl, string downstreamPath) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); route.RouteIsCaseSensitive = true; var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Hello from Guillaume")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestUrl)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Theory] [Trait("Bug", "2212")] [InlineData("/data-registers/{version}/it/{everything}", "/dati-registri/{version}/{everything}", "/dati-registri/v1.0/operatore/R80QQ5J9600/valida", "/data-registers/v1.0/it/operatore/R80QQ5J9600/valida")] [InlineData("/files/{version}/uploads/{everything}", "/data/{version}/storage/{everything}", "/data/v2.0/storage/images/photos/nature", "/files/v2.0/uploads/images/photos/nature")] [InlineData("/resources/{area}/details/{everything}", "/api/resources/{area}/info/{everything}", "/api/resources/global/info/stats/2024/data", "/resources/global/details/stats/2024/data")] [InlineData("/users/{userId}/logs/{everything}", "/data/users/{userId}/activity/{everything}", "/data/users/12345/activity/session/login/2024", "/users/12345/logs/session/login/2024")] [InlineData("/orders/{orderId}/items/{everything}", "/ecommerce/{orderId}/details/{everything}", "/ecommerce/98765/details/category/electronics/phone", "/orders/98765/items/category/electronics/phone")] [InlineData("/tasks/{taskId}/subtasks/{everything}", "/work/{taskId}/breakdown/{everything}", "/work/56789/breakdown/phase/3/step/2", "/tasks/56789/subtasks/phase/3/step/2")] [InlineData("/configs/{env}/overrides/{everything}", "/settings/{env}/{everything}", "/settings/prod/feature/toggles", "/configs/prod/overrides/feature/toggles")] public void OnlyTheLastPlaceholderShouldMatchSeveralSegments(string downstream, string upstream, string requestUrl, string downstreamPath) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => GivenThereIsAServiceRunningOn(port, downstreamPath, HttpStatusCode.OK, "Hello from Guillaume")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(requestUrl)) .Then(x => ThenTheDownstreamUrlPathShouldBe(downstreamPath)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Guillaume")) .BDDfy(); } [Fact] [Trait("Feat", "91, 94")] public void Should_return_response_201_with_simple_url_and_multiple_upstream_http_method() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithMethods(HttpMethods.Get, HttpMethods.Post); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.Created, nameof(HttpStatusCode.Created))) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", "postContent")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Created)) .And(x => ThenTheResponseBodyShouldBe(nameof(HttpStatusCode.Created))) .BDDfy(); } [Fact] [Trait("Feat", "91, 94")] public void Should_return_response_200_with_simple_url_and_any_upstream_http_method() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithMethods(); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "134")] public void Should_return_404_when_calling_upstream_route_with_no_matching_downstream_route() { var port = PortFinder.GetRandomPort(); var methods = new string[] { HttpMethods.Options, HttpMethods.Put, HttpMethods.Get, HttpMethods.Post, HttpMethods.Delete }; var route1 = GivenRoute(port, "/vacancy/", "/api/v1/vacancy").WithMethods(methods); var route2 = GivenRoute(port, "/vacancy/{vacancyId}", "/api/v1/vacancy/{vacancyId}").WithMethods(methods); route1.LoadBalancerOptions = route2.LoadBalancerOptions = new(nameof(LeastConnection)); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/vacancy/1", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("api/vacancy/1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] [Trait("Bug", "145")] public void Should_not_set_trailing_slash_on_url_template() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/platform/{url}", "/api/{url}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/swagger/lib/backbone-min.js", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/platform/swagger/lib/backbone-min.js")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe("/api/swagger/lib/backbone-min.js")) .BDDfy(); } [Fact] [Trait("Feat", "270, 272")] public void Should_use_priority() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/goods/{url}", "/goods/{url}") .WithPriority(0); var route2 = GivenRoute(port2, "/goods/delete", "/goods/delete"); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port2, "/goods/delete", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/goods/delete")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "548")] public void Should_match_multiple_paths_with_catch_all() { var port = PortFinder.GetRandomPort(); var route = GivenCatchAllRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/test/toot", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/test/toot")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "271")] public void Should_fix_issue_271() { var port = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port, "/api/v1/{everything}", "/api/v1/{everything}") .WithMethods(HttpMethods.Get, HttpMethods.Put, HttpMethods.Post); var route2 = GivenRoute(port2, "/connect/token", "/connect/token") .WithMethods(HttpMethods.Post); var configuration = GivenConfiguration(route1, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/api/v1/modules/Test", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/api/v1/modules/Test")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Theory] [Trait("Bug", "2116")] [InlineData("debug()")] // no query [InlineData("debug%28%29")] // debug() public void Should_change_downstream_path_by_upstream_path_when_path_contains_malicious_characters(string path) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/{path}", "/routed/api/{path}"); var configuration = GivenConfiguration(route); var decodedDownstreamUrlPath = $"/routed/api/{HttpUtility.UrlDecode(path)}"; this.Given(x => x.GivenThereIsAServiceRunningOn(port, decodedDownstreamUrlPath, HttpStatusCode.OK, string.Empty)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/{path}")) // should be encoded .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheDownstreamUrlPathShouldBe(decodedDownstreamUrlPath)) .BDDfy(); } [Fact] [Trait("Bug", "2064")] [Trait("Discus", "2065")] public void Should_match_correct_route_when_placeholder_appears_after_query_start() { const string DownstreamPath = "/1/products/1"; var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration( GivenRoute(port, "/{tenantId}/products?{everything}", "/{tenantId}/products?{everything}"), // This route should NOT BE matched GivenRoute(port, "/{tenantId}/products/{everything}", "/{tenantId}/products/{everything}")); // This route should BE matched this.Given(x => GivenThereIsAServiceRunningOn(port, DownstreamPath, HttpStatusCode.OK, "Hello from Finn")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/1/products/1")) .Then(x => ThenTheDownstreamUrlPathShouldBe(DownstreamPath)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Finn")) .BDDfy(); } [Fact] [Trait("Bug", "2132")] public void Should_match_correct_route_when_a_configuration_exists_with_query_param_wildcard() { const string DownstreamPath = "/api/v1/apple"; var port = PortFinder.GetRandomPort(); var configuration = GivenConfiguration( GivenRoute(port, "/api/v1/abc?{everything}", "/api/v1/abc?{everything}"), // This route should NOT be matched GivenRoute(port, "/api/v1/abc2/{everything}", "/api/v1/{everything}")); // This route should be matched this.Given(x => GivenThereIsAServiceRunningOn(port, DownstreamPath, HttpStatusCode.OK, "Hello from Finn")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/api/v1/abc2/apple?isRequired=1")) .Then(x => ThenTheDownstreamUrlPathShouldBe(DownstreamPath)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe("?isRequired=1")) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Finn")) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatusCode); Task MapStatusCode(HttpContext context) { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value + context.Request.Path.Value : context.Request.Path.Value; _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty; bool oK = _downstreamPath == basePath; context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(oK ? responseBody : "Downstream path didn't match base path"); } } private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPath) { _downstreamPath.ShouldBe(expectedDownstreamPath); } private void ThenTheDownstreamUrlQueryStringShouldBe(string expectedQueryString) { _downstreamQuery.ShouldBe(expectedQueryString); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Routing/RoutingWithQueryStringTests.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.AcceptanceTests.Routing; public sealed class RoutingWithQueryStringTests : Steps { public RoutingWithQueryStringTests() { } [Fact] public void Should_return_response_200_with_query_string_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/units/{subscriptionId}/{unitId}/updates", "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/api/subscriptions/{subscriptionId}/updates")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?unitId={unitId}")) .BDDfy(); } [Theory] [Trait("Bug", "952")] [InlineData("")] [InlineData("&x=xxx")] public void Should_return_200_with_query_string_template_different_keys(string additionalParams) { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/units/{subscriptionId}/updates?unit={unit}", "/api/subscriptions/{subscriptionId}/updates?unitId={unit}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/subscriptions/{subscriptionId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/api/subscriptions/{subscriptionId}/updates")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?unitId={unitId}{additionalParams}")) .BDDfy(); } [Fact] [Trait("Bug", "952")] public void Should_map_query_parameters_with_different_names() { const string userId = "webley"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/users?userId={userId}", "/persons?personId={userId}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", "Hello from @webley")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userId}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) .And(x => ThenTheDownstreamUrlPathShouldBe("/persons")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?personId={userId}")) .BDDfy(); } [Fact] [Trait("Bug", "952")] public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() { const string uid = "webley"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/users?userId={uid}", "/persons?personId={uid}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", "Hello from @webley")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={uid}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) .And(x => ThenTheDownstreamUrlPathShouldBe("/persons")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?personId={uid}&userId={uid}")) .BDDfy(); } [Fact] [Trait("Bug", "952")] public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ_case_sensitive() { const string userid = "webley"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/users?userId={userid}", "/persons?personId={userid}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/persons", "Hello from @webley")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/users?userId={userid}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from @webley")) .And(x => ThenTheDownstreamUrlPathShouldBe("/persons")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?personId={userid}&userId={userid}")) .BDDfy(); } [Theory] [Trait("Bug", "1174")] // https://github.com/ThreeMammals/Ocelot/issues/1174 [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "?projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "?$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00%3A00%3A00z%20and%20DateOfSale%20le%202020-03-15T00%3A00%3A00z")] public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expected) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/contracts?{everythingelse}", "/api/contracts?{everythingelse}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/contracts", "Hello from @sunilk3")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from @sunilk3")) .And(x => ThenTheDownstreamUrlPathShouldBe("/api/contracts")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(expected)) .BDDfy(); } [Fact] [Trait("Bug", "548")] // https://github.com/ThreeMammals/Ocelot/issues/548 [Trait("Commit", "00a6000")] // https://github.com/ThreeMammals/Ocelot/commit/00a600064deea0877058d04e6189d7e0278c99a5 [Trait("Release", "10.0.4")] public void Should_return_response_200_with_odata_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenCatchAllRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/odata/customers", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe("/odata/customers")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe("?$filter=Name%20eq%20%27Sam%27")) .BDDfy(); } [Fact] [Trait("Feat", "467")] public void Should_return_response_200_with_query_string_upstream_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", "/api/units/{subscriptionId}/{unitId}/updates"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/api/units/{subscriptionId}/{unitId}/updates")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty)) .BDDfy(); } [Fact] [Trait("Feat", "467")] public void Should_return_response_404_with_query_string_upstream_template_no_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", "/api/units/{subscriptionId}/{unitId}/updates"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] [Trait("Feat", "467")] public void Should_return_response_404_with_query_string_upstream_template_different_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", "/api/units/{subscriptionId}/{unitId}/updates"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } [Fact] [Trait("Feat", "467")] public void Should_return_response_200_with_query_string_upstream_template_multiple_params() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", "/api/units/{subscriptionId}/{unitId}/updates"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/api/units/{subscriptionId}/{unitId}/updates", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/api/units/{subscriptionId}/{unitId}/updates")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe("?productId=1")) .BDDfy(); } [Fact] [Trait("Bug", "2002")] // https://github.com/ThreeMammals/Ocelot/issues/2002 [Trait("Commit", "a034e8c")] // https://github.com/ThreeMammals/Ocelot/commit/a034e8c1e3fc23a086ad10000c85615b9696a43e [Trait("Release", "23.3.0")] public void Should_map_when_query_parameters_has_same_names_with_placeholder() { const string username = "bbenameur"; const string groupName = "Paris"; const string roleid = "123456"; const string everything = "something=9874565"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}", "/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/account/{username}/groups/{groupName}/roles", "Hello from Béchir")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Béchir")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/account/{username}/groups/{groupName}/roles")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?roleId={roleid}&{everything}")) .BDDfy(); } /// /// To reproduce 1288: query string should contain the placeholder name and value. /// [Fact] [Trait("Bug", "1288")] public void Should_copy_query_string_to_downstream_path() { var idName = "id"; var idValue = "3"; var queryName = idName + "1"; var queryValue = "2" + idValue + "12"; var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, $"/safe/{{{idName}}}", $"/cpx/t1/{{{idName}}}"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, $"/cpx/t1/{idValue}", "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => ThenTheDownstreamUrlPathShouldBe($"/cpx/t1/{idValue}")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe($"?{queryName}={queryValue}")) .BDDfy(); } #region PR 2351 [Fact] [Trait("PR", "2351")] // https://github.com/ThreeMammals/Ocelot/pull/2351 [Trait("Bug", "2346")] // https://github.com/ThreeMammals/Ocelot/issues/2346 public void Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder_as_a_Catch_All_Query_String() { // This test ensures that a placeholder like {id} does not remove or corrupt parameters like customer_id const string customer_id = "12345"; var port = PortFinder.GetRandomPort(); var route1 = GivenRoute(port, upstream: "/finance/v1/payment-methods?{id}", // Catch All query string placeholder -> https://ocelot.readthedocs.io/en/latest/features/routing.html#catch-all-query-string downstream: "/v1/payment-methods?{id}"); var route2 = GivenRoute(port, upstream: "/finance/v1/payment-methods", downstream: "/v1/payment-methods"); var configuration = GivenConfiguration(route1, route2); var query = $"?{nameof(customer_id)}={customer_id}"; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/v1/payment-methods", "Hello from Bhargav")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(route1.UpstreamPathTemplate.Replace("?{id}", query))) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("Hello from Bhargav")) .And(x => ThenTheDownstreamUrlPathShouldBe("/v1/payment-methods")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(query)) .When(x => WhenIGetUrlOnTheApiGateway(route2.UpstreamPathTemplate)) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheDownstreamUrlPathShouldBe("/v1/payment-methods")) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(string.Empty)) .BDDfy(); } [Theory] [Trait("PR", "2351")] // https://github.com/ThreeMammals/Ocelot/pull/2351 [Trait("Bug", "2346")] // https://github.com/ThreeMammals/Ocelot/issues/2346 [InlineData("/finance/v1/payment-methods/{id}", "/v1/payment-methods/{id}", // Placeholder: {id}, Query: customer_id "/finance/v1/payment-methods/?customer_id=123", "/v1/payment-methods/", "?customer_id=123")] [InlineData("/finance/v1/payment-methods/{id}/orders/{oid}", "/v1/orders/{id}/{oid}", // Placeholder: {id}, Query: orderid, customer_id "/finance/v1/payment-methods/1/orders/2?id=1&oid=2&orderid=3&customer_id=4", "/v1/orders/1/2", "?orderid=3&customer_id=4")] [InlineData("/finance/v1/users/{customer_id}", "/v1/users/{customer_id}", // Placeholder: {customer_id}, Query: id "/finance/v1/users/123?id=999&customer_id=x", "/v1/users/123", "?id=999")] [InlineData("/finance/v1/items/{id}/{Id}", "/v1/items/{Id}/{id}", // Placeholder: {Id}, Query: id, Id "/finance/v1/items/1/2?id=3&Id=4&ID=5&extra=x", // case insensitive params will be removed "/v1/items/2/1", "?extra=x")] [InlineData("/finance/v1/data/{id}", "/v1/data/{id}", // Placeholder: {id}, Query: xid, idx, id "/finance/v1/data/123/?xid=1&idx=2&id=3&extra=x", "/v1/data/123/", "?xid=1&idx=2&extra=x")] [InlineData("/finance/v1/records/{id1}", "/v1/records/{id1}", // Placeholder: {id1}, Query: id1, id10 "/finance/v1/records/123/?id1=1&id10=10&extra=x", "/v1/records/123/", "?id10=10&extra=x")] [InlineData("/finance/v1/alpha/{id}", "/v1/alpha/{id}", // Placeholder: {id}, Query: id_, _id, id "/finance/v1/alpha/123/?id_=1&_id=2&id=3&extra=x", "/v1/alpha/123/", "?id_=1&_id=2&extra=x")] [InlineData("/finance/v2/orders/{id}", "/v2/orders/{id}", // Placeholder: {id}, Query: multiple query parameters "/finance/v2/orders/42?status=active&customer_id=123&extra=x", "/v2/orders/42", "?status=active&customer_id=123&extra=x")] public void Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder( string upstream, string downstream, string url, string path, string query) { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, upstream, downstream); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, path, "Hello from Bhargav")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(url)) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("Hello from Bhargav")) .And(x => ThenTheDownstreamUrlPathShouldBe(path)) .And(x => ThenTheDownstreamUrlQueryStringShouldBe(query)) .BDDfy(); } #endregion of PR 2351 private void GivenThereIsAServiceRunningOn(int port, string basePath, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatusCode); Task MapStatusCode(HttpContext context) { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value + context.Request.Path.Value : context.Request.Path.Value; _downstreamQuery = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : string.Empty; bool oK = _downstreamPath == basePath; // || _downstreamQuery != queryString; // TODO this is strict assertion, it can be without query string context.Response.StatusCode = oK ? StatusCodes.Status200OK : StatusCodes.Status404NotFound; return context.Response.WriteAsync(oK ? responseBody : "Downstream path didn't match base path"); } } private string _downstreamPath; private string _downstreamQuery; private void ThenTheDownstreamUrlPathShouldBe(string expected) => _downstreamPath.ShouldBe(expected); private void ThenTheDownstreamUrlQueryStringShouldBe(string expected) => _downstreamQuery.ShouldBe(expected); } ================================================ FILE: test/Ocelot.AcceptanceTests/Routing/UpstreamHostTests.cs ================================================ using Microsoft.AspNetCore.Http; namespace Ocelot.AcceptanceTests.Routing; /// /// Feature: Upstream Host, /// with docs: Upstream Host. /// public sealed class UpstreamHostTests : Steps { public UpstreamHostTests() { } [Fact] public void Should_return_response_200_with_simple_url_and_hosts_match() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithUpstreamHost("localhost"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes() { var port = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithUpstreamHost("localhost"); var route2 = GivenDefaultRoute(port2).WithUpstreamHost("DONTMATCH"); var configuration = GivenConfiguration(route, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed() { var port = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithUpstreamHost("DONTMATCH"); var route2 = GivenDefaultRoute(port2).WithUpstreamHost("localhost"); var configuration = GivenConfiguration(route, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port2, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed_with_no_host_first() { var port = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithUpstreamHost(null); var route2 = GivenDefaultRoute(port2).WithUpstreamHost("localhost"); var configuration = GivenConfiguration(route, route2); this.Given(x => x.GivenThereIsAServiceRunningOn(port2, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_return_response_404_with_simple_url_and_hosts_dont_match() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port).WithUpstreamHost("127.0.0.20:5000"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool oK = downstreamPath == basePath; context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(oK ? responseBody : "downstream path didn't match base path"); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Security/SecurityOptionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests.Security; public sealed class SecurityOptionsTests: Steps { public SecurityOptionsTests() { } [Fact] [Trait("Feat", "2170")] public void Should_call_with_allowed_ip_in_global_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.35")[0]; var route = GivenRoute(port, "/myPath", "/worldPath"); var configuration = GivenGlobalConfiguration(route, "192.168.1.30-50", "192.168.1.1-100"); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)); } [Fact] [Trait("Feat", "2170")] public void Should_block_call_with_blocked_ip_in_global_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.55")[0]; var route = GivenRoute(port, "/myPath", "/worldPath"); var configuration = GivenGlobalConfiguration(route, "192.168.1.30-50", "192.168.1.1-100"); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)); } [Fact] public void Should_call_with_allowed_ip_in_route_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.1")[0]; var securityConfig = new FileSecurityOptions { IPAllowedList = new() { "192.168.1.1" }, }; var route = GivenRoute(port, "/myPath", "/worldPath", securityConfig); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)); } [Fact] public void Should_block_call_with_blocked_ip_in_route_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.1")[0]; var securityConfig = new FileSecurityOptions { IPBlockedList = new() { "192.168.1.1" }, }; var route = GivenRoute(port, "/myPath", "/worldPath", securityConfig); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)); } [Fact] [Trait("Feat", "2170")] public void Should_call_with_allowed_ip_in_route_config_and_blocked_ip_in_global_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.55")[0]; var securityConfig = new FileSecurityOptions { IPAllowedList = new() { "192.168.1.55" }, }; var route = GivenRoute(port, "/myPath", "/worldPath", securityConfig); var configuration = GivenGlobalConfiguration(route, "192.168.1.30-50", "192.168.1.1-100"); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .Then(x => ThenTheResponseBodyShouldBe("Hello from Fabrizio")); } [Fact] [Trait("Feat", "2170")] public void Should_block_call_with_blocked_ip_in_route_config_and_allowed_ip_in_global_config() { var port = PortFinder.GetRandomPort(); var ip = Dns.GetHostAddresses("192.168.1.35")[0]; var securityConfig = new FileSecurityOptions { IPBlockedList = new() { "192.168.1.35" }, }; var route = GivenRoute(port, "/myPath", "/worldPath", securityConfig); var configuration = GivenGlobalConfiguration(route, "192.168.1.30-50", "192.168.1.1-100"); this.Given(x => x.GivenThereIsAServiceRunningOn(port, ip)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/worldPath")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Unauthorized)); } private void GivenThereIsAServiceRunningOn(int port, IPAddress ipAddess) { handler.GivenThereIsAServiceRunningOn(port, context => { context.Connection.RemoteIpAddress = ipAddess; context.Response.StatusCode = (int)HttpStatusCode.OK; return context.Response.WriteAsync("Hello from Fabrizio"); }); } private FileConfiguration GivenGlobalConfiguration(FileRoute route, string allowed, string blocked, bool exclude = true) { var config = GivenConfiguration(route); config.GlobalConfiguration.SecurityOptions = new FileSecurityOptions { IPAllowedList = new() { allowed }, IPBlockedList = new() { blocked }, ExcludeAllowedFromBlocked = exclude, }; return config; } private static FileRoute GivenRoute(int port, string downstream, string upstream, FileSecurityOptions fileSecurityOptions = null) => new() { DownstreamPathTemplate = downstream, UpstreamPathTemplate = upstream, UpstreamHttpMethod = [HttpMethods.Get], SecurityOptions = fileSecurityOptions ?? new(), }; } ================================================ FILE: test/Ocelot.AcceptanceTests/SequentialTests.cs ================================================ [assembly: CollectionBehavior(DisableTestParallelization = true)] namespace Ocelot.AcceptanceTests; /// /// Apply to classes to disable parallelization. /// [CollectionDefinition(nameof(SequentialTests), DisableParallelization = true)] public class SequentialTests { } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulAgentServiceExtensions.cs ================================================ using Consul; namespace Ocelot.AcceptanceTests.ServiceDiscovery; internal static class ConsulAgentServiceExtensions { public static AgentService WithServiceName(this AgentService agent, string serviceName) { agent.Service = serviceName; return agent; } public static AgentService WithPort(this AgentService agent, int port) { agent.Port = port; return agent; } public static AgentService WithAddress(this AgentService agent, string address) { agent.Address = address; return agent; } public static ServiceEntry ToServiceEntry(this AgentService agent) => new() { Service = agent, }; } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulConfigurationInConsulTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Ocelot.AcceptanceTests.RateLimiting; using Ocelot.Cache; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; using System.Text; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class ConsulConfigurationInConsulTests : RateLimitingSteps { private FileConfiguration _config; private readonly List _consulServices; public ConsulConfigurationInConsulTests() { _consulServices = new List(); } [Fact] public void Should_return_response_200_with_simple_url() { var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = servicePort, }, }, UpstreamPathTemplate = "/", UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; this.Given(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty)) .And(x => x.GivenThereIsAServiceRunningOn(servicePort, string.Empty, HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_load_configuration_out_of_consul() { var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; var consulConfig = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/status", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = servicePort, }, }, UpstreamPathTemplate = "/cs/status", UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty)) .And(x => x.GivenThereIsAServiceRunningOn(servicePort, "/status", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig()) .When(x => WhenIGetUrlOnTheApiGateway("/cs/status")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_load_configuration_out_of_consul_if_it_is_changed() { var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; var consulConfig = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/status", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = servicePort, }, }, UpstreamPathTemplate = "/cs/status", UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; var secondConsulConfig = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/status", DownstreamScheme = "http", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = servicePort, }, }, UpstreamPathTemplate = "/cs/status/awesome", UpstreamHttpMethod = ["Get"], }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; this.Given(x => GivenTheConsulConfigurationIs(consulConfig)) .And(x => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, string.Empty)) .And(x => x.GivenThereIsAServiceRunningOn(servicePort, "/status", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig()) .And(x => WhenIGetUrlOnTheApiGateway("/cs/status")) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .When(x => GivenTheConsulConfigurationIs(secondConsulConfig)) .Then(x => ThenTheConfigIsUpdatedInOcelot()) .BDDfy(); } [Fact] public void Should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() { var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; var servicePort = PortFinder.GetRandomPort(); var serviceEntryOne = new ServiceEntry { Service = new AgentService { Service = serviceName, Address = "localhost", Port = servicePort, ID = "web_90_0_2_224_8080", Tags = new[] { "version-v1" }, }, }; var consulConfig = new FileConfiguration { DynamicRoutes = new() { new() { ServiceName = serviceName, RateLimitRule = new FileRateLimitByHeaderRule { EnableRateLimiting = true, ClientWhitelist = new List(), Limit = 3, Period = "1s", PeriodTimespan = 1000, }, }, }, GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { Scheme = "http", Host = "localhost", Port = consulPort, }, RateLimitOptions = new() { ClientIdHeader = "ClientId", QuotaExceededMessage = string.Empty, RateLimitCounterPrefix = string.Empty, HttpStatusCode = StatusCodes.Status428PreconditionRequired, }, DownstreamScheme = "http", }, }; var configuration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { Scheme = "http", Host = "localhost", Port = consulPort, }, }, }; var upstreamPath = $"/{serviceName}/something"; this.Given(x => x.GivenThereIsAServiceRunningOn(servicePort, "/something", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenTheConsulConfigurationIs(consulConfig)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, serviceName)) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningUsingConsulToStoreConfig()) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1)) .Then(x => ThenTheStatusCodeShouldBe(200)) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 2)) .Then(x => ThenTheStatusCodeShouldBe(200)) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimes(upstreamPath, 1)) .Then(x => ThenTheStatusCodeShouldBe(428)) .BDDfy(); } private async Task ThenTheConfigIsUpdatedInOcelot() { var result = await Wait.For(20_000).UntilAsync(async () => { try { await WhenIGetUrlOnTheApiGateway("/cs/status/awesome"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenTheResponseBodyShouldBe("Hello from Laura"); return true; } catch (Exception) { return false; } }); result.ShouldBeTrue(); } private void GivenTheConsulConfigurationIs(FileConfiguration config) { _config = config; } private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) { foreach (var serviceEntry in serviceEntries) { _consulServices.Add(serviceEntry); } } private Task GivenOcelotIsRunningUsingConsulToStoreConfig() { static void WithConsulToStoreConfig(IServiceCollection services) => services.AddOcelot().AddConsul().AddConfigStoredInConsul(); GivenOcelotIsRunning(WithConsulToStoreConfig); return Task.Delay(1000); } private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port, string serviceName) { handler.GivenThereIsAServiceRunningOn(port, async context => { if (context.Request.Method.Equals(HttpMethods.Get, StringComparison.CurrentCultureIgnoreCase) && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { var json = JsonConvert.SerializeObject(_config); var bytes = Encoding.UTF8.GetBytes(json); var base64 = Convert.ToBase64String(bytes); var kvp = new FakeConsulGetResponse(base64); json = JsonConvert.SerializeObject(new[] { kvp }); context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); } else if (context.Request.Method.Equals(HttpMethods.Put, StringComparison.CurrentCultureIgnoreCase) && context.Request.Path.Value == "/v1/kv/InternalConfiguration") { try { var reader = new StreamReader(context.Request.Body); // Synchronous operations are disallowed. Call ReadAsync or set AllowSynchronousIO to true instead. // var json = reader.ReadToEnd(); var json = await reader.ReadToEndAsync(); _config = JsonConvert.DeserializeObject(json); var response = JsonConvert.SerializeObject(true); await context.Response.WriteAsync(response); } catch (Exception e) { Console.WriteLine(e); throw; } } else if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { var json = JsonConvert.SerializeObject(_consulServices); context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); } }); } public class FakeConsulGetResponse { public FakeConsulGetResponse(string value) => Value = value; public int CreateIndex => 100; public int ModifyIndex => 200; public int LockIndex => 200; public string Key => "InternalConfiguration"; public int Flags => 0; public string Value { get; } public string Session => "adf4238a-882b-9ddc-4a9d-5b6758e4159e"; } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { Task MapStatus(HttpContext context) { context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(responseBody); } handler.GivenThereIsAServiceRunningOn(port, basePath, MapStatus); } private class FakeCache : IOcelotCache { public FileConfiguration Get(string key, string region) => throw new NotImplementedException(); public void ClearRegion(string region) => throw new NotImplementedException(); public bool TryGetValue(string key, string region, out FileConfiguration value) => throw new NotImplementedException(); public bool Add(string key, FileConfiguration value, string region, TimeSpan ttl) => throw new NotImplementedException(); public FileConfiguration AddOrUpdate(string key, FileConfiguration value, string region, TimeSpan ttl) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulIntegrationTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using System.Runtime.CompilerServices; using ConsulProvider = Ocelot.Provider.Consul.Consul; namespace Ocelot.AcceptanceTests.ServiceDiscovery; // [Collection(nameof(SequentialTests))] public class ConsulIntegrationTests : Steps { private readonly int _consulPort; private readonly string _consulHost; private readonly string _consulScheme; private readonly List _consulServiceEntries; private readonly Mock _factory; private readonly Mock _logger; private readonly Mock _contextAccessor; private IConsulClientFactory _clientFactory; private IConsulServiceBuilder _serviceBuilder; private ConsulRegistryConfiguration _config; private ConsulProvider _provider; private string _receivedToken; public ConsulIntegrationTests() { _consulPort = PortFinder.GetRandomPort(); _consulHost = "localhost"; _consulScheme = Uri.UriSchemeHttp; _consulServiceEntries = new List(); _factory = new Mock(); _logger = new Mock(); _contextAccessor = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); } private void Arrange([CallerMemberName] string serviceName = null) { _config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _consulPort, serviceName, null); var context = new DefaultHttpContext(); context.Items.Add(nameof(ConsulRegistryConfiguration), _config); _contextAccessor.SetupGet(x => x.HttpContext).Returns(context); _clientFactory = new ConsulClientFactory(); _serviceBuilder = new DefaultConsulServiceBuilder(_contextAccessor.Object, _clientFactory, _factory.Object); _provider = new ConsulProvider(_config, _factory.Object, _clientFactory, _serviceBuilder); } [Fact] public async Task Should_return_service_from_consul() { Arrange(); var service1 = GivenService(PortFinder.GetRandomPort()); _consulServiceEntries.Add(service1.ToServiceEntry()); GivenThereIsAFakeConsulServiceDiscoveryProvider(); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().Count.ShouldBe(1); } [Fact] public async Task Should_use_token() { Arrange(); const string token = "test token"; var service1 = GivenService(PortFinder.GetRandomPort()); _consulServiceEntries.Add(service1.ToServiceEntry()); GivenThereIsAFakeConsulServiceDiscoveryProvider(); var config = new ConsulRegistryConfiguration(_consulScheme, _consulHost, _consulPort, nameof(Should_use_token), token); _provider = new ConsulProvider(config, _factory.Object, _clientFactory, _serviceBuilder); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().Count.ShouldBe(1); _receivedToken.ShouldBe(token); } [Fact] public async Task Should_not_return_services_with_invalid_address() { Arrange(); var service1 = GivenService(PortFinder.GetRandomPort(), "http://localhost"); var service2 = GivenService(PortFinder.GetRandomPort(), "http://localhost"); _consulServiceEntries.Add(service1.ToServiceEntry()); _consulServiceEntries.Add(service2.ToServiceEntry()); GivenThereIsAFakeConsulServiceDiscoveryProvider(); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().Count.ShouldBe(0); ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); } [Fact] public async Task Should_not_return_services_with_empty_address() { Arrange(); var service1 = GivenService(PortFinder.GetRandomPort()).WithAddress(string.Empty); var service2 = GivenService(PortFinder.GetRandomPort()).WithAddress(null); _consulServiceEntries.Add(service1.ToServiceEntry()); _consulServiceEntries.Add(service2.ToServiceEntry()); GivenThereIsAFakeConsulServiceDiscoveryProvider(); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().Count.ShouldBe(0); ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); } [Fact] public async Task Should_not_return_services_with_invalid_port() { Arrange(); var service1 = GivenService(-1); var service2 = GivenService(0); _consulServiceEntries.Add(service1.ToServiceEntry()); _consulServiceEntries.Add(service2.ToServiceEntry()); GivenThereIsAFakeConsulServiceDiscoveryProvider(); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().Count.ShouldBe(0); ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(); } [Fact] public async Task GetAsync_NoEntries_ShouldLogWarning() { Arrange(); _consulServiceEntries.Clear(); // NoEntries _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); GivenThereIsAFakeConsulServiceDiscoveryProvider(); // Act var actual = await _provider.GetAsync(); // Assert actual.ShouldNotBeNull().ShouldBeEmpty(); var expected = $"Consul Provider: No service entries found for '{nameof(GetAsync_NoEntries_ShouldLogWarning)}' service!"; _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); } private static AgentService GivenService(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() { Service = serviceName, Address = address ?? "localhost", Port = port, ID = id ?? Guid.NewGuid().ToString(), Tags = tags ?? Array.Empty(), }; private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning() { foreach (var entry in _consulServiceEntries) { var service = entry.Service; var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); } } private void GivenThereIsAFakeConsulServiceDiscoveryProvider([CallerMemberName] string serviceName = "test") { string url = $"{_consulScheme}://{_consulHost}:{_consulPort}"; handler.GivenThereIsAServiceRunningOn(url, async context => { if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) { _receivedToken = values.First(); } var json = JsonConvert.SerializeObject(_consulServiceEntries); context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); } }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulServiceDiscoveryTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Ocelot.AcceptanceTests.LoadBalancer; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Creators; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace Ocelot.AcceptanceTests.ServiceDiscovery; /// /// Tests for the provider. /// public sealed partial class ConsulServiceDiscoveryTests : ConcurrentSteps, IDisposable { private readonly ServiceHandler _consulHandler; private readonly List _consulServices; private readonly List _consulNodes; private string _receivedToken; private volatile int _counterConsul; private volatile int _counterNodes; public ConsulServiceDiscoveryTests() { _consulHandler = new ServiceHandler(); _consulServices = new List(); _consulNodes = new List(); } public override void Dispose() { _consulHandler?.Dispose(); base.Dispose(); } [Fact] [Trait("Feat", "28")] public void ShouldDiscoverServicesInConsulAndLoadBalanceByLeastConnectionWhenConfigInRoute() { const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(2); var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray(); var route = GivenDiscoveryRoute(serviceName: serviceName, loadBalancerType: nameof(LeastConnection)); var configuration = GivenServiceDiscovery(consulPort, route); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 50)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*25*/24, /*25*/26)) // TODO Check strict assertion .BDDfy(); } private static readonly string[] VersionV1Tags = new[] { "version-v1" }; private static readonly string[] GetVsOptionsMethods = new[] { "Get", "Options" }; [Fact] [Trait("Feat", "201")] [Trait("Bug", "213")] public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequest() { const string serviceName = "web"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var serviceEntryOne = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", VersionV1Tags, serviceName); var route = GivenDiscoveryRoute("/api/home", "/home", serviceName, httpMethods: GetVsOptionsMethods); var configuration = GivenServiceDiscovery(consulPort, route); this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryOne)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGateway("/home")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Bug", "213")] [Trait("Feat", "201")] [Trait("Feat", "340")] public void ShouldHandleRequestToConsulForDownstreamServiceAndMakeRequestWhenDynamicRoutingWithNoRoutes() { const string serviceName = "web"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", VersionV1Tags, serviceName); var configuration = GivenServiceDiscovery(consulPort); configuration.GlobalConfiguration.DownstreamScheme = "http"; configuration.GlobalConfiguration.HttpHandlerOptions = new() { AllowAutoRedirect = true, UseCookieContainer = true, UseTracing = false, }; this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/something", "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGateway("/web/something")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] [Trait("Feat", "340")] public void ShouldUseConsulServiceDiscoveryAndLoadBalanceRequestWhenDynamicRoutingWithNoRoutes() { const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(2); var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray(); var configuration = GivenServiceDiscovery(consulPort); configuration.GlobalConfiguration.LoadBalancerOptions = new() { Type = nameof(LeastConnection) }; configuration.GlobalConfiguration.DownstreamScheme = "http"; var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", 50)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*25*/24, /*25*/26)) // TODO Check strict assertion .BDDfy(); } [Fact] [Trait("Feat", "295")] public void ShouldUseAclTokenToMakeRequestToConsul() { const string serviceName = "web"; const string token = "abctoken"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var serviceEntry = GivenServiceEntry(servicePort, "localhost", "web_90_0_2_224_8080", VersionV1Tags, serviceName); var route = GivenDiscoveryRoute("/api/home", "/home", serviceName, httpMethods: GetVsOptionsMethods); var configuration = GivenServiceDiscovery(consulPort, route); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Token = token; this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGateway("/home")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .And(x => x.ThenTheTokenIs(token)) .BDDfy(); } [Fact] [Trait("Bug", "181")] public void ShouldSendRequestToServiceAfterItBecomesAvailableInConsul() { const string serviceName = "product"; var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(2); var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray(); var route = GivenDiscoveryRoute(serviceName: serviceName); var configuration = GivenServiceDiscovery(consulPort, route); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(_ => GivenMultipleServiceInstancesAreRunning(urls, serviceName)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .And(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 10)) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(10)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*5*/4, /*5*/6)) // TODO Check strict assertion .And(x => x.WhenIRemoveAService(serviceEntries[1])) // 2nd entry .And(x => x.GivenIResetCounters()) .And(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 10)) .And(x => ThenServicesShouldHaveBeenCalledTimes(10, 0)) // 2nd is offline .And(x => x.WhenIAddAServiceBackIn(serviceEntries[1])) // 2nd entry .And(x => x.GivenIResetCounters()) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently("/", 10)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(10)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(/*5*/4, /*5*/6)) // TODO Check strict assertion .BDDfy(); } [Fact] [Trait("Feat", "374")] public void ShouldPollConsulForDownstreamServiceAndMakeRequest() { const string serviceName = "web"; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); var serviceEntry = GivenServiceEntry(servicePort, "localhost", $"web_90_0_2_224_{servicePort}", VersionV1Tags, serviceName); var route = GivenDiscoveryRoute("/api/home", "/home", serviceName, httpMethods: GetVsOptionsMethods); var configuration = GivenServiceDiscovery(consulPort, route); var sd = configuration.GlobalConfiguration.ServiceDiscoveryProvider; sd.Type = nameof(PollConsul); // !!! sd.PollingInterval = 0; sd.Namespace = string.Empty; this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", "Hello from Laura")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk("/home")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } private async Task WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) { var result = await Wait.For(2_000).UntilAsync(async () => { try { response = await ocelotClient.GetAsync(url); response.EnsureSuccessStatusCode(); return true; } catch (Exception) { return false; } }); result.ShouldBeTrue(); } [Theory] [Trait("Bug", "849")] [Trait("Bug", "1496")] [Trait("PR", "1944")] [InlineData(nameof(NoLoadBalancer))] [InlineData(nameof(RoundRobin))] [InlineData(nameof(LeastConnection))] [InlineData(nameof(CookieStickySessions))] public void ShouldUseConsulServiceDiscoveryWhenThereAreTwoUpstreamHosts(string loadBalancerType) { // Simulate two DIFFERENT downstream services (e.g. product services for US and EU markets) // with different ServiceNames (e.g. product-us and product-eu), // UpstreamHost is used to determine which ServiceName to use when making a request to Consul (e.g. Host: us-shop goes to product-us) const string serviceNameUS = "product-us"; const string serviceNameEU = "product-eu"; string[] tagsUS = new[] { "US" }, tagsEU = new[] { "EU" }; var consulPort = PortFinder.GetRandomPort(); var servicePortUS = PortFinder.GetRandomPort(); var servicePortEU = PortFinder.GetRandomPort(); const string upstreamHostUS = "us-shop"; const string upstreamHostEU = "eu-shop"; const string responseBodyUS = "Phone chargers with US plug"; const string responseBodyEU = "Phone chargers with EU plug"; var serviceEntryUS = GivenServiceEntry(servicePortUS, serviceName: serviceNameUS, tags: tagsUS); var serviceEntryEU = GivenServiceEntry(servicePortEU, serviceName: serviceNameEU, tags: tagsEU); var routeUS = GivenDiscoveryRoute("/products", "/", serviceNameUS, loadBalancerType, upstreamHostUS); var routeEU = GivenDiscoveryRoute("/products", "/", serviceNameEU, loadBalancerType, upstreamHostEU); var configuration = GivenServiceDiscovery(consulPort, routeUS, routeEU); bool isStickySession = loadBalancerType == nameof(CookieStickySessions); var sessionCookieUS = isStickySession ? new CookieHeaderValue(routeUS.LoadBalancerOptions.Key, Guid.NewGuid().ToString()) : null; var sessionCookieEU = isStickySession ? new CookieHeaderValue(routeEU.LoadBalancerOptions.Key, Guid.NewGuid().ToString()) : null; // Ocelot request for http://us-shop/ should find 'product-us' in Consul, call /products and return "Phone chargers with US plug" // Ocelot request for http://eu-shop/ should find 'product-eu' in Consul, call /products and return "Phone chargers with EU plug" this.Given(x => handler.GivenThereIsAServiceRunningOn(servicePortUS, "/products", MapGet("/products", responseBodyUS))) .Given(x => handler.GivenThereIsAServiceRunningOn(servicePortEU, "/products", MapGet("/products", responseBodyEU))) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntryUS, serviceEntryEU)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeUS.UpstreamPathTemplate, upstreamHostUS, sessionCookieUS), "When I get US shop for the first time") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(1)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeEU.UpstreamPathTemplate, upstreamHostEU, sessionCookieEU), "When I get EU shop for the first time") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(2)) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeUS.UpstreamPathTemplate, upstreamHostUS, sessionCookieUS), "When I get US shop again") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(isStickySession ? 2 : 3)) // sticky sessions use cache, so Consul shouldn't be called .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(responseBodyUS)) .When(x => x.WhenIGetUrlOfRequestComingFromHost(routeEU.UpstreamPathTemplate, upstreamHostEU, sessionCookieEU), "When I get EU shop again") .Then(x => x.ThenConsulShouldHaveBeenCalledTimes(isStickySession ? 2 : 4)) // sticky sessions use cache, so Consul shouldn't be called .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe(responseBodyEU)) .BDDfy(); } [Fact] [Trait("Bug", "954")] public void ShouldReturnServiceAddressByOverriddenServiceBuilderWhenThereIsANode() { const string serviceName = "OpenTestService"; string[] methods = new[] { HttpMethods.Post, HttpMethods.Get }; var consulPort = PortFinder.GetRandomPort(); var servicePort = PortFinder.GetRandomPort(); // 9999 var serviceEntry = GivenServiceEntry(servicePort, id: "OPEN_TEST_01", serviceName: serviceName, tags: new[] { serviceName }); var serviceNode = new Node() { Name = "n1" }; // cornerstone of the bug serviceEntry.Node = serviceNode; var route = GivenDiscoveryRoute("/api/{url}", "/open/{url}", serviceName, httpMethods: methods); var configuration = GivenServiceDiscovery(consulPort, route); this.Given(x => GivenThereIsAServiceRunningOn(DownstreamUrl(servicePort), "/api/home", "Hello from Raman")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntry)) .And(x => x.GivenTheServiceNodesAreRegisteredWithConsul(serviceNode)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) // default services registration results with the bug: "n1" host issue .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .And(x => ThenTheResponseBodyShouldBe("")) .And(x => ThenConsulShouldHaveBeenCalledTimes(1)) .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(1)) // Override default service builder .Given(x => GivenOcelotIsRunning(WithConsulServiceBuilder)) .When(x => WhenIGetUrlOnTheApiGateway("/open/home")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Raman")) .And(x => ThenConsulShouldHaveBeenCalledTimes(2)) .And(x => ThenConsulNodesShouldHaveBeenCalledTimes(2)) .BDDfy(); } private static readonly string[] Bug2119ServiceNames = new string[] { "ProjectsService", "CustomersService" }; private readonly ILoadBalancer[] _lbAnalyzers = new ILoadBalancer[Bug2119ServiceNames.Length]; // emulate LoadBalancerHouse's collection private TLoadBalancer GetAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider) where TLoadBalancer : class, ILoadBalancer where TLoadBalancerCreator : class, ILoadBalancerCreator, new() { //lock (LoadBalancerHouse.SyncRoot) // Note, synch locking is implemented in LoadBalancerHouse int index = Array.IndexOf(Bug2119ServiceNames, route.ServiceName); // LoadBalancerHouse should return different balancers for different service names _lbAnalyzers[index] ??= new TLoadBalancerCreator().Create(route, provider)?.Data; return (TLoadBalancer)_lbAnalyzers[index]; } private void WithLbAnalyzer(IServiceCollection services) where TLoadBalancer : class, ILoadBalancer where TLoadBalancerCreator : class, ILoadBalancerCreator, new() => services.AddOcelot().AddConsul().AddCustomLoadBalancer(GetAnalyzer); [Theory] [Trait("Bug", "2119")] [InlineData(nameof(NoLoadBalancer))] [InlineData(nameof(RoundRobin))] [InlineData(nameof(LeastConnection))] // original scenario public void ShouldReturnDifferentServicesWhenThereAre2SequentialRequestsToDifferentServices(string loadBalancer) { var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length); var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]); var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenDiscoveryRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenDiscoveryRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) // Step 1 .When(x => WhenIGetUrlOnTheApiGateway("/projects/api/projects")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenServiceShouldHaveBeenCalledTimes(0, 1)) .And(x => x.ThenTheResponseBodyShouldBe($"1^:^{Bug2119ServiceNames[0]}")) // ! // Step 2 .When(x => WhenIGetUrlOnTheApiGateway("/customers/api/customers")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenServiceShouldHaveBeenCalledTimes(1, 1)) .And(x => x.ThenTheResponseBodyShouldBe($"1^:^{Bug2119ServiceNames[1]}")) // !! // Finally .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK)) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(2)) .And(x => ThenServicesShouldHaveBeenCalledTimes(1, 1)) .BDDfy(); } [Theory] [Trait("Bug", "2119")] [InlineData(false, nameof(NoLoadBalancer))] [InlineData(false, nameof(LeastConnection))] // original scenario, clean config [InlineData(true, nameof(LeastConnectionAnalyzer))] // extended scenario using analyzer [InlineData(false, nameof(RoundRobin))] [InlineData(true, nameof(RoundRobinAnalyzer))] public void ShouldReturnDifferentServicesWhenSequentiallyRequestingToDifferentServices(bool withAnalyzer, string loadBalancer) { var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length); var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]); var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenDiscoveryRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenDiscoveryRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); Func requestToProjectsAndThenRequestToCustomersAndAssert = async (i) => { // Step 1 int count = i + 1; await WhenIGetUrlOnTheApiGateway("/projects/api/projects"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenServiceShouldHaveBeenCalledTimes(0, count); ThenTheResponseBodyShouldBe($"{count}^:^{Bug2119ServiceNames[0]}", $"i is {i}"); _responses[2 * i] = response; // Step 2 await WhenIGetUrlOnTheApiGateway("/customers/api/customers"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenServiceShouldHaveBeenCalledTimes(1, count); ThenTheResponseBodyShouldBe($"{count}^:^{Bug2119ServiceNames[1]}", $"i is {i}"); _responses[(2 * i) + 1] = response; }; this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) // service names as responses .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(withAnalyzer ? WithLbAnalyzer(loadBalancer) : WithConsul)) .When(x => WhenIDoActionMultipleTimes(50, requestToProjectsAndThenRequestToCustomersAndAssert)) .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK)) .And(x => x.ThenResponsesShouldHaveBodyFromDifferentServices(ports, Bug2119ServiceNames)) // !!! .And(x => ThenAllServicesShouldHaveBeenCalledTimes(100)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(50, 50)) .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 50)) // strict assertion .BDDfy(); } [Theory] [Trait("Bug", "2119")] [InlineData(false, nameof(NoLoadBalancer))] [InlineData(false, nameof(LeastConnection))] // original scenario, clean config [InlineData(true, nameof(LeastConnectionAnalyzer))] // extended scenario using analyzer [InlineData(false, nameof(RoundRobin))] [InlineData(true, nameof(RoundRobinAnalyzer))] public void ShouldReturnDifferentServicesWhenConcurrentlyRequestingToDifferentServices(bool withAnalyzer, string loadBalancer) { const int total = 100; // concurrent requests var consulPort = PortFinder.GetRandomPort(); var ports = PortFinder.GetPorts(Bug2119ServiceNames.Length); var service1 = GivenServiceEntry(ports[0], serviceName: Bug2119ServiceNames[0]); var service2 = GivenServiceEntry(ports[1], serviceName: Bug2119ServiceNames[1]); var route1 = GivenDiscoveryRoute("/{all}", "/projects/{all}", serviceName: Bug2119ServiceNames[0], loadBalancerType: loadBalancer); var route2 = GivenDiscoveryRoute("/{all}", "/customers/{all}", serviceName: Bug2119ServiceNames[1], loadBalancerType: loadBalancer); route1.UpstreamHttpMethod = route2.UpstreamHttpMethod = new() { HttpMethods.Get, HttpMethods.Post, HttpMethods.Put, HttpMethods.Delete }; var configuration = GivenServiceDiscovery(consulPort, route1, route2); var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, Bug2119ServiceNames)) // service names as responses .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(service1, service2)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(withAnalyzer ? WithLbAnalyzer(loadBalancer) : WithConsul)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently(total, "/projects/api/projects", "/customers/api/customers")) .Then(x => ThenAllStatusCodesShouldBe(HttpStatusCode.OK)) .And(x => x.ThenResponsesShouldHaveBodyFromDifferentServices(ports, Bug2119ServiceNames)) // !!! .And(x => ThenAllServicesShouldHaveBeenCalledTimes(total)) .And(x => ThenServiceCountersShouldMatchLeasingCounters((ILoadBalancerAnalyzer)_lbAnalyzers[0], ports, 50)) // ProjectsService .And(x => ThenServiceCountersShouldMatchLeasingCounters((ILoadBalancerAnalyzer)_lbAnalyzers[1], ports, 50)) // CustomersService .And(x => ThenAllServicesCalledRealisticAmountOfTimes(Bottom(total, ports.Length), Top(total, ports.Length))) .And(x => ThenServicesShouldHaveBeenCalledTimes(50, 50)) // strict assertion .BDDfy(); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes() { var ports = PortFinder.GetPorts(5); var serviceName = ServiceName(); var serviceEntries = ports.Select(port => GivenServiceEntry(port, serviceName: serviceName)).ToArray(); var consulPort = PortFinder.GetRandomPort(); var configuration = GivenServiceDiscovery(consulPort); configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp; configuration.Routes = []; // dynamic routing configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes var urls = ports.Select(DownstreamUrl).ToArray(); this.Given(x => GivenMultipleServiceInstancesAreRunning(urls, serviceName)) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(DownstreamUrl(consulPort))) .And(x => x.GivenTheServicesAreRegisteredWithConsul(serviceEntries)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithConsul)) .When(x => WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", 50)) .Then(x => ThenAllServicesShouldHaveBeenCalledTimes(50)) .And(x => ThenAllServicesCalledRealisticAmountOfTimes(9, 11)) // soft assertion .And(x => ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10)) // distribution by RoundRobin algorithm, aka strict assertion .BDDfy(); } private Action WithLbAnalyzer(string loadBalancer) => loadBalancer switch { nameof(LeastConnection) => WithLbAnalyzer, nameof(LeastConnectionAnalyzer) => WithLbAnalyzer, nameof(RoundRobin) => WithLbAnalyzer, nameof(RoundRobinAnalyzer) => WithLbAnalyzer, _ => WithLbAnalyzer, }; private void ThenResponsesShouldHaveBodyFromDifferentServices(int[] ports, string[] serviceNames) { foreach (var response in _responses) { var headers = response.Value.Headers; headers.TryGetValues(HeaderNames.ServiceIndex, out var indexValues).ShouldBeTrue(); int serviceIndex = int.Parse(indexValues.FirstOrDefault() ?? "-1"); serviceIndex.ShouldBeGreaterThanOrEqualTo(0); headers.TryGetValues(HeaderNames.Host, out var hostValues).ShouldBeTrue(); hostValues.FirstOrDefault().ShouldBe("localhost"); headers.TryGetValues(HeaderNames.Port, out var portValues).ShouldBeTrue(); portValues.FirstOrDefault().ShouldBe(ports[serviceIndex].ToString()); var body = response.Value.Content.ReadAsStringAsync().Result; var serviceName = serviceNames[serviceIndex]; body.ShouldNotBeNull().ShouldEndWith(serviceName); headers.TryGetValues(HeaderNames.Counter, out var counterValues).ShouldBeTrue(); var counter = counterValues.ShouldNotBeNull().FirstOrDefault().ShouldNotBeNull(); body.ShouldBe($"{counter}^:^{serviceName}"); } } private static void WithConsul(IServiceCollection services) => services .AddOcelot().AddConsul(); private static void WithConsulServiceBuilder(IServiceCollection services) => services .AddOcelot().AddConsul(); public class MyConsulServiceBuilder : DefaultConsulServiceBuilder { public MyConsulServiceBuilder(IHttpContextAccessor contextAccessor, IConsulClientFactory clientFactory, IOcelotLoggerFactory loggerFactory) : base(contextAccessor, clientFactory, loggerFactory) { } protected override string GetDownstreamHost(ServiceEntry entry, Node node) => entry.Service.Address; } private static ServiceEntry GivenServiceEntry(int port, string address = null, string id = null, string[] tags = null, [CallerMemberName] string serviceName = null) => new() { Service = new AgentService { Service = serviceName, Address = address ?? "localhost", Port = port, ID = id ?? Guid.NewGuid().ToString(), Tags = tags ?? Array.Empty(), }, }; private FileRoute GivenDiscoveryRoute(string downstream = null, string upstream = null, [CallerMemberName] string serviceName = null, string loadBalancerType = null, string upstreamHost = null, string[] httpMethods = null) => new() { DownstreamPathTemplate = downstream ?? "/", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = upstream ?? "/", UpstreamHttpMethod = httpMethods != null ? new(httpMethods) : [HttpMethods.Get], UpstreamHost = upstreamHost, ServiceName = serviceName, LoadBalancerOptions = new() { Type = loadBalancerType ?? nameof(LeastConnection), Key = serviceName, Expiry = 60_000, }, }; private FileConfiguration GivenServiceDiscovery(int consulPort, params FileRoute[] routes) { var config = GivenConfiguration(routes); config.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = Uri.UriSchemeHttp, Host = "localhost", Port = consulPort, Type = nameof(Provider.Consul.Consul), }; return config; } private async Task WhenIGetUrlOfRequestComingFromHost(string url, string requestHost, CookieHeaderValue cookie) { var request = new HttpRequestMessage(HttpMethod.Get, url); request.Headers.Add(nameof(HttpRequestHeaders.Host), requestHost); // ! if (cookie != null) request.Headers.Add("Cookie", cookie.ToString()); response = await ocelotClient.ShouldNotBeNull().SendAsync(request); } private void ThenTheTokenIs(string token) { _receivedToken.ShouldBe(token); } private void WhenIAddAServiceBackIn(ServiceEntry serviceEntry) { _consulServices.Add(serviceEntry); } private void WhenIRemoveAService(ServiceEntry serviceEntry) { _consulServices.Remove(serviceEntry); } private void GivenIResetCounters() { _counters[0] = _counters[1] = 0; _counterConsul = 0; } private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) => _consulServices.AddRange(serviceEntries); private void GivenTheServiceNodesAreRegisteredWithConsul(params Node[] nodes) => _consulNodes.AddRange(nodes); [GeneratedRegex("/v1/health/service/(?[^/]+)", RegexOptions.Singleline, RegexGlobal.DefaultMatchTimeoutMilliseconds)] private static partial Regex ServiceNameRegex(); private void GivenThereIsAFakeConsulServiceDiscoveryProvider(string url) { _consulHandler.GivenThereIsAServiceRunningOn(url, async context => { if (context.Request.Headers.TryGetValue("X-Consul-Token", out var values)) { _receivedToken = values.First(); } // Parse the request path to get the service name var pathMatch = ServiceNameRegex().Match(context.Request.Path.Value); if (pathMatch.Success) { //string json; //lock (ConsulCounterLocker) //{ //_counterConsul++; int count = Interlocked.Increment(ref _counterConsul); // Use the parsed service name to filter the registered Consul services var serviceName = pathMatch.Groups["serviceName"].Value; var services = _consulServices.Where(x => x.Service.Service == serviceName).ToList(); var json = JsonConvert.SerializeObject(services); //} context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); return; } if (context.Request.Path.Value == "/v1/catalog/nodes") { //_counterNodes++; int count = Interlocked.Increment(ref _counterNodes); var json = JsonConvert.SerializeObject(_consulNodes); context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); } }); } private void ThenConsulShouldHaveBeenCalledTimes(int expected) => _counterConsul.ShouldBe(expected); private void ThenConsulNodesShouldHaveBeenCalledTimes(int expected) => _counterNodes.ShouldBe(expected); } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulTwoDownstreamServicesTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class ConsulTwoDownstreamServicesTests : Steps { private readonly List _serviceEntries; public ConsulTwoDownstreamServicesTests() { _serviceEntries = new List(); } [Fact] [Trait("Bug", "194")] // https://github.com/ThreeMammals/Ocelot/issues/194 public void Should_fix_issue_194() { var consulPort = PortFinder.GetRandomPort(); var servicePort1 = PortFinder.GetRandomPort(); var servicePort2 = PortFinder.GetRandomPort(); var route1 = GivenRoute(servicePort1, "/api/user/{user}", "/api/user/{user}"); var route2 = GivenRoute(servicePort2, "/api/product/{product}", "/api/product/{product}"); var configuration = GivenConfiguration(route1, route2); configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = Uri.UriSchemeHttps, Host = "localhost", Port = consulPort, }; this.Given(x => x.GivenProductServiceIsRunning(servicePort1, "/api/user/info", HttpStatusCode.OK, "user")) .And(x => x.GivenProductServiceIsRunning(servicePort2, "/api/product/info", HttpStatusCode.OK, "product")) .And(x => x.GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/api/user/info?id=1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("user")) .When(x => WhenIGetUrlOnTheApiGateway("/api/product/info?id=1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("product")) .BDDfy(); } private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port) { handler.GivenThereIsAServiceRunningOn(port, context => { if (context.Request.Path.Value == "/v1/health/service/product") { var json = JsonConvert.SerializeObject(_serviceEntries); context.Response.Headers.Append("Content-Type", "application/json"); return context.Response.WriteAsync(json); } return context.Response.WriteAsync(string.Empty); }); } private void GivenProductServiceIsRunning(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool oK = downstreamPath == basePath; context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(oK ? responseBody : "downstream path didn't match base path"); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ConsulWebSocketTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using Ocelot.AcceptanceTests.WebSockets; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; using System.Text; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class ConsulWebSocketTests : WebSocketsSteps { private readonly List _serviceEntries = new(); [Fact] public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() { var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; var secondDownstreamPort = PortFinder.GetRandomPort(); var secondDownstreamHost = "localhost"; var serviceName = "websockets"; var consulPort = PortFinder.GetRandomPort(); var serviceEntryOne = new ServiceEntry { Service = new AgentService { Service = serviceName, Address = downstreamHost, Port = downstreamPort, ID = Guid.NewGuid().ToString(), Tags = Array.Empty(), }, }; var serviceEntryTwo = new ServiceEntry { Service = new AgentService { Service = serviceName, Address = secondDownstreamHost, Port = secondDownstreamPort, ID = Guid.NewGuid().ToString(), Tags = Array.Empty(), }, }; var config = new FileConfiguration { Routes = new List { new() { UpstreamPathTemplate = "/", DownstreamPathTemplate = "/ws", DownstreamScheme = "ws", LoadBalancerOptions = new FileLoadBalancerOptions { Type = "RoundRobin" }, ServiceName = serviceName, }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "http", Host = "localhost", Port = consulPort, Type = "consul", }, }, }; int ocelotPort = PortFinder.GetRandomPort(); this.Given(_ => GivenThereIsAConfiguration(config)) .And(_ => StartOcelotWithWebSockets(ocelotPort, WithConsul)) .And(_ => GivenThereIsAFakeConsulServiceDiscoveryProvider(consulPort, serviceName)) .And(_ => GivenTheServicesAreRegisteredWithConsul(serviceEntryOne, serviceEntryTwo)) .And(_ => GivenWebSocketsServiceIsRunningAsync(downstreamPort, "/ws", EchoAsync, CancellationToken.None)) .And(_ => GivenWebSocketsServiceIsRunningAsync(secondDownstreamPort, "/ws", MessageAsync, CancellationToken.None)) .When(_ => WhenIStartTheClients(ocelotPort)) .Then(_ => ThenBothDownstreamServicesAreCalled()) .BDDfy(); } private void WithConsul(IServiceCollection services) => services.AddOcelot().AddConsul(); private void GivenTheServicesAreRegisteredWithConsul(params ServiceEntry[] serviceEntries) { foreach (var serviceEntry in serviceEntries) { _serviceEntries.Add(serviceEntry); } } private void GivenThereIsAFakeConsulServiceDiscoveryProvider(int port, string serviceName) { Task MapServicePath(HttpContext context) { if (context.Request.Path.Value == $"/v1/health/service/{serviceName}") { var json = JsonConvert.SerializeObject(_serviceEntries); context.Response.Headers.Append("Content-Type", "application/json"); return context.Response.WriteAsync(json); } return Task.CompletedTask; } handler.GivenThereIsAServiceRunningOn(port, MapServicePath); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/DynamicRoutingTests.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.DependencyInjection; using Ocelot.AcceptanceTests.Authentication; using Ocelot.AcceptanceTests.Caching; using Ocelot.AcceptanceTests.QualityOfService; using Ocelot.AcceptanceTests.Requester; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer.Balancers; using Ocelot.Logging; using Ocelot.Metadata; using Ocelot.Provider.Polly; using Ocelot.Requester; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Testing.Authentication; using Ocelot.Values; namespace Ocelot.AcceptanceTests.ServiceDiscovery; /// /// These tests are based on the custom service discovery provider, abstracting from currently implemented discovery providers and focusing on the dynamic routing features. /// public class DynamicRoutingTests : ConcurrentSteps { [Fact] [Trait("Feat", "351")] public void ShouldForwardQueryStringToDownstream() { var ports = PortFinder.GetPorts(2); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); GivenMultipleServiceInstancesAreRunning(serviceUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscovery); var pathWithQueryString = $"/{serviceName}/?{nameof(TestID)}={TestID}"; WhenIGetUrlOnTheApiGatewayConcurrently(pathWithQueryString, 2); ThenAllServicesShouldHaveBeenCalledTimes(2); ThenServicesShouldHaveBeenCalledTimes(1, 1); var pathAndQuery = ThenAllResponsesHeaderExists(HeaderNames.Path).ToList(); pathAndQuery.ShouldAllBe(pathQuery => pathWithQueryString.Contains(pathQuery)); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes() { var ports = PortFinder.GetPorts(5); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); GivenMultipleServiceInstancesAreRunning(serviceUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscovery); WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", 50); ThenAllServicesShouldHaveBeenCalledTimes(50); ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void ShouldApplyGlobalGroupLoadBalancerOptions_ForDynamicRoutes_WhenRouteOptsHasAKey() { // 1st route var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute("route1", key: null); // 1st route is not in the global group route1.LoadBalancerOptions = null; // 1st route is not balanced GivenDiscoveryMetadata(route1, ports1); // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute("route2", key: "R2"); // 2nd route is in the group route2.LoadBalancerOptions = null; // 2nd route opts will be applied from global ones GivenDiscoveryMetadata(route2, ports2); // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute("noLoadBalancing", loadBalancer: nameof(NoLoadBalancer), key: null); GivenDiscoveryMetadata(route3, ports3); var configuration = GivenDynamicRouting(new(), route1, route2, route3); configuration.GlobalConfiguration.LoadBalancerOptions = new() { RouteKeys = ["R2"], Type = nameof(RoundRobin), }; var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscovery); WhenIGetUrlOnTheApiGatewayConcurrently("/route1/", 2); WhenIGetUrlOnTheApiGatewayConcurrently("/route2/", 4); WhenIGetUrlOnTheApiGatewayConcurrently("/noLoadBalancing/", 5); ThenServicesShouldHaveBeenCalledTimes(2, 0, 2, 2, 5, 0); // main assertion, explanation is below ThenServiceShouldHaveBeenCalledTimes(0, 2); // NoLoadBalancer for 2 ThenServiceShouldHaveBeenCalledTimes(1, 0); // NoLoadBalancer for 2 ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 4 ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 4 ThenServiceShouldHaveBeenCalledTimes(4, 5); // NoLoadBalancer for 5 ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 5 } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] [Trait("PR", "2331")] // https://github.com/ThreeMammals/Ocelot/pull/2331 public void ShouldApplyGlobalCacheOptions_ForAllDynamicRoutes() { const int TTL = 1; // let's cache for one second var ports = PortFinder.GetPorts(2); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); configuration.GlobalConfiguration.CacheOptions = new() { TtlSeconds = TTL, // Let's cache for one second }; var (testBody1, testBody2) = CachingTests.TestBodiesFactory(); GivenMultipleServiceInstancesAreRunning(serviceUrls, responses: [testBody1, testBody2]); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscovery); AssertCachedRoute(TTL, serviceName, ports, [testBody1, testBody2]); } private void AssertCachedRoute(int ttl, string serviceName, int[] ports, string[] expectedBody, bool cached = true, bool balanced = true, int shift = 0) { Array.Clear(_counters); var url = $"/{serviceName}/"; WhenIGetUrlOnTheApiGatewayConcurrently(url, 2); ThenAllServicesShouldHaveBeenCalledTimes(2); //ThenServicesShouldHaveBeenCalledTimes(1, 1); // distribution by RoundRobin algorithm, aka strict assertion ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? 1 : 2); ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? 1 : 0); GivenIWait(100); WhenIGetUrlOnTheApiGatewayConcurrently(url, 2); ThenAllServicesShouldHaveBeenCalledTimes(cached ? 2 : 4); // the counters remain unchanged, and the items are still in the cache int counter = cached ? 1 : 2; //ThenServicesShouldHaveBeenCalledTimes(counter, counter); // the counters remain unchanged ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? counter : 2 * counter); ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? counter : 0); GivenIWait(ttl * 1000); // allow cached items to expire WhenIGetUrlOnTheApiGatewayConcurrently(url, 2); ThenAllServicesShouldHaveBeenCalledTimes(cached ? 4 : 6); // the counters have been updated because new items were added to the cache counter = cached ? 2 : 3; //ThenServicesShouldHaveBeenCalledTimes(counter, counter); // the counters have been updated ThenServiceShouldHaveBeenCalledTimes(shift + 0, balanced ? counter : 2 * counter); ThenServiceShouldHaveBeenCalledTimes(shift + 1, balanced ? counter : 0); ThenAllResponseBodiesShouldBe(ports, expectedBody); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] [Trait("PR", "2331")] // https://github.com/ThreeMammals/Ocelot/pull/2331 public void ShouldApplyGlobalGroupCacheOptions_WhenRouteOptsHasAKey() { const int TTL = 1; // let's cache for one second // 1st route var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute("route1", key: null); // 1st route is not in the global group route1.CacheOptions = null; // 1st route is not cached GivenDiscoveryMetadata(route1, ports1); // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute("route2", key: "R2"); // 2nd route is in the group route2.CacheOptions = null; // 2nd route opts will be applied from global ones GivenDiscoveryMetadata(route2, ports2); // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute("noCaching", loadBalancer: nameof(NoLoadBalancer), key: null); GivenDiscoveryMetadata(route3, ports3); var configuration = GivenDynamicRouting(new(), route1, route2, route3); configuration.GlobalConfiguration.CacheOptions = new() { RouteKeys = ["R2"], Region = "global", Header = "global", TtlSeconds = TTL, }; var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); var (testBody1, testBody2) = CachingTests.TestBodiesFactory(); GivenMultipleServiceInstancesAreRunning(downstreamUrls, responses: [testBody1, testBody2, testBody1, testBody2, testBody1, testBody2]); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscovery); int length = _counters.Length; AssertCachedRoute(TTL, route1.ServiceName, ports1, [testBody1, testBody2], cached: false, shift: 0); int[] counters1 = new int[length]; Array.Copy(_counters, counters1, length); AssertCachedRoute(TTL, route2.ServiceName, ports2, [testBody1, testBody2], cached: true, shift: 2); int[] counters2 = new int[length]; Array.Copy(_counters, counters2, length); AssertCachedRoute(TTL, route3.ServiceName, ports3, [testBody1, testBody2], cached: false, balanced: false, shift: 4); int[] counters3 = new int[length]; Array.Copy(_counters, counters3, length); for (int i = 0; i < length; i++) { _counters[i] = counters1[i] + counters2[i] + counters3[i]; } ThenServicesShouldHaveBeenCalledTimes(3, 3, 2, 2, 6, 0); // main assertion, explanation is below ThenServiceShouldHaveBeenCalledTimes(0, 3); // RoundRobin for 6, not cached ThenServiceShouldHaveBeenCalledTimes(1, 3); // RoundRobin for 6, not cached ThenServiceShouldHaveBeenCalledTimes(2, 2); // RoundRobin for 6, cached 1 ThenServiceShouldHaveBeenCalledTimes(3, 2); // RoundRobin for 6, cached 1 ThenServiceShouldHaveBeenCalledTimes(4, 6); // NoLoadBalancer for 6, not cached ThenServiceShouldHaveBeenCalledTimes(5, 0); // NoLoadBalancer for 6, not cached } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void ShouldApplyGlobalHttpHandlerOptions_ForAllDynamicRoutes() { var ports = PortFinder.GetPorts(3); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); configuration.GlobalConfiguration.HttpHandlerOptions = new() { MaxConnectionsPerServer = 77, PooledConnectionLifetimeSeconds = 88, UseTracing = true, // let's enable global tracing }; GivenMultipleServiceInstancesAreRunning(serviceUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscoveryAndRequesterTesting); int times = ports.Length; WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", times); ThenAllServicesShouldHaveBeenCalledTimes(times); ThenServicesShouldHaveBeenCalledTimes(1, 1, 1); // distribution by RoundRobin algorithm, aka strict assertion ThenRouteHttpHandlerOptionsAre(serviceName, configuration.GlobalConfiguration.Metadata, 77, 88, true); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void ShouldApplyGlobalGroupHttpHandlerOptions_ForDynamicRoutes_WhenRouteOptsHasAKey() { // 1st route var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute("route1", key: null); // 1st route is not in the global group route1.HttpHandlerOptions = null; // 1st route has no opts GivenDiscoveryMetadata(route1, ports1); // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute("route2", key: "R2"); // 2nd route is in the group route2.HttpHandlerOptions = null; // 2nd route opts will be applied from global ones GivenDiscoveryMetadata(route2, ports2); // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute("noTracing", loadBalancer: nameof(NoLoadBalancer), key: null); var route3Opts = route3.HttpHandlerOptions = new() { MaxConnectionsPerServer = 66, PooledConnectionLifetimeSeconds = 77, UseTracing = false, // no tracing route }; GivenDiscoveryMetadata(route3, ports3); var configuration = GivenDynamicRouting(new(), route1, route2, route3); var globalOpts = configuration.GlobalConfiguration.HttpHandlerOptions = new() { RouteKeys = ["R2"], MaxConnectionsPerServer = 88, PooledConnectionLifetimeSeconds = 99, UseCookieContainer = false, UseProxy = false, UseTracing = true, // enable global tracing }; var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscoveryAndRequesterTesting); WhenIGetUrlOnTheApiGatewayConcurrently("/route1/", 2); WhenIGetUrlOnTheApiGatewayConcurrently("/route2/", 2); WhenIGetUrlOnTheApiGatewayConcurrently("/noTracing/", 2); ThenServicesShouldHaveBeenCalledTimes(1, 1, 1, 1, 2, 0); ThenRouteHttpHandlerOptionsAre(route1.ServiceName, route1.Metadata, int.MaxValue, HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, false); // default opts ThenRouteHttpHandlerOptionsAre(route2.ServiceName, route2.Metadata, globalOpts.MaxConnectionsPerServer.Value, globalOpts.PooledConnectionLifetimeSeconds.Value, globalOpts.UseTracing.Value); // global opts ThenRouteHttpHandlerOptionsAre(route3.ServiceName, route3.Metadata, route3Opts.MaxConnectionsPerServer.Value, route3Opts.PooledConnectionLifetimeSeconds.Value, route3Opts.UseTracing.Value); // route opts } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 public async Task ShouldApplyGlobalAuthenticationOptions_ForAllDynamicRoutes() { using var steps = new AuthenticationSteps(); var ports = PortFinder.GetPorts(3); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); configuration.GlobalConfiguration.AuthenticationOptions = new(AuthenticationSteps.GivenOptions(false, ["apiGlobal"], [JwtBearerDefaults.AuthenticationScheme])); GivenMultipleServiceInstancesAreRunning(serviceUrls, Enumerable.Repeat(serviceName, ports.Length).ToArray()); steps.GivenThereIsAConfiguration(configuration); steps.GivenOcelotIsRunning(WithDiscoveryAndJwtBearerAuthentication(steps)); await steps.GivenThereIsExternalJwtSigningService(["apiGlobal"], Xunit.TestContext.Current.CancellationToken); await steps.GivenIHaveAToken(scope: "apiGlobal"); //,audience: ocelotClient.BaseAddress.Authority); steps.GivenIHaveAddedATokenToMyRequest(); int times = ports.Length; ocelotClient ??= steps.OcelotClient; WhenIGetUrlOnTheApiGatewayConcurrently($"/{serviceName}/", times); ThenAllServicesShouldHaveBeenCalledTimes(times); ThenServicesShouldHaveBeenCalledTimes(1, 1, 1); // distribution by RoundRobin algorithm, aka strict assertion ThenAllStatusCodesShouldBe(HttpStatusCode.OK); ThenAllResponseBodiesShouldBe(serviceName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 public async Task ShouldApplyGlobalGroupAuthenticationOptions_ForDynamicRoutes_WhenRouteOptsHasAKey() { using var steps = new AuthenticationSteps(); // 1st route var ports1 = PortFinder.GetPorts(2); var route1 = GivenLbRoute("route1", key: null); // 1st route is not in the global group route1.AuthenticationOptions = null; // 1st route has no opts GivenDiscoveryMetadata(route1, ports1); // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute("route2", key: "R2"); // 2nd route is in the group route2.AuthenticationOptions = null; // 2nd route opts will be applied from global ones GivenDiscoveryMetadata(route2, ports2); // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute("noAuthorization", loadBalancer: nameof(NoLoadBalancer), key: null); var route3Opts = route3.AuthenticationOptions = AuthenticationSteps.GivenOptions(false, ["invalid-scope"], [JwtBearerDefaults.AuthenticationScheme]); GivenDiscoveryMetadata(route3, ports3); var configuration = GivenDynamicRouting(new(), route1, route2, route3); var globalOptions = configuration.GlobalConfiguration.AuthenticationOptions = new(AuthenticationSteps.GivenOptions(false, ["apiGlobal"], [JwtBearerDefaults.AuthenticationScheme])) { RouteKeys = ["R2"], }; var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls, Enumerable.Repeat(Body(), downstreamUrls.Length).ToArray()); steps.GivenThereIsAConfiguration(configuration); steps.GivenOcelotIsRunning(WithDiscoveryAndJwtBearerAuthentication(steps)); await steps.GivenThereIsExternalJwtSigningService(["api", "apiGlobal", "Mr.Who"], Xunit.TestContext.Current.CancellationToken); ocelotClient ??= steps.OcelotClient; await steps.GivenIHaveAToken(scope: "Mr.Who"); steps.GivenIHaveAddedATokenToMyRequest(); WhenIGetUrlOnTheApiGatewayConcurrently("/route1/", 2); ThenAllStatusCodesShouldBe(HttpStatusCode.OK); // auth is switched off and the scope doesn't matter ThenAllResponseBodiesShouldBe(Body()); await steps.GivenIHaveAToken(scope: globalOptions.AllowedScopes[0]); steps.GivenIHaveAddedATokenToMyRequest(); WhenIGetUrlOnTheApiGatewayConcurrently("/route2/", 2); ThenAllStatusCodesShouldBe(HttpStatusCode.OK); // global scope has been accepted ThenAllResponseBodiesShouldBe(Body()); await steps.GivenIHaveAToken(scope: "Mr.Who"); // should be different scope of route #3 which is "invalid-scope" steps.GivenIHaveAddedATokenToMyRequest(); WhenIGetUrlOnTheApiGatewayConcurrently("/noAuthorization/", 2); ThenAllStatusCodesShouldBe(HttpStatusCode.Forbidden); ThenAllResponseBodiesShouldBe("0"); ThenServicesShouldHaveBeenCalledTimes(1, 1, 1, 1, 0, 0); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public async Task ShouldApplyGlobalQosOptions_ForAllDynamicRoutes() { var ports = PortFinder.GetPorts(3); var serviceName = ServiceName(); var serviceUrls = ports.Select(DownstreamUrl).ToArray(); var configuration = GivenDynamicRouting(new() { { serviceName, serviceUrls }, }); FileQoSOptions globalOptions = configuration.GlobalConfiguration.QoSOptions = new() { BreakDuration = CircuitBreakerStrategy.LowBreakDuration + 1, // 501 MinimumThroughput = 2, // exceptions-errors Timeout = 500, // ms }; GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscoveryAndPolly); using var steps = new QosSteps(this); _counters = new int[serviceUrls.Length]; steps.CounterStrategy = (port) => { int index = Array.FindIndex(serviceUrls, url => new Uri(url).Port == port); int count = Interlocked.Increment(ref _counters[index]); }; await steps.TestRouteCircuitBreaker(ports, $"/{serviceName}/", globalOptions, isDiscovery: true); // test global scenario await steps.TestRouteTimeout(ports, $"/{serviceName}/", globalOptions); ThenServicesShouldHaveBeenCalledTimes(2, 2, 1); } [Fact] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public async Task ShouldApplyGlobalQosOptions_ForAllDynamicRoutes_WithGroupedOpts() { const int GlobalTimeout = 1500, GlobalExceptions = 3, GlobalBreakMs = 2000; var ports1 = PortFinder.GetPorts(2); // 1st route var route1 = GivenLbRoute("route1", key: null); // 1st route is not in the global group route1.QoSOptions = null; // 1st route has no opts GivenDiscoveryMetadata(route1, ports1); // 2nd route var ports2 = PortFinder.GetPorts(2); var route2 = GivenLbRoute("route2", key: "R2"); // 2nd route is in the group route2.QoSOptions = null; // 2nd route opts will be applied from global ones GivenDiscoveryMetadata(route2, ports2); // 3rd route var ports3 = PortFinder.GetPorts(2); var route3 = GivenLbRoute("noCircuitBreaker", loadBalancer: nameof(NoLoadBalancer), key: null); route3.QoSOptions = new() { MinimumThroughput = 0, // disable Circuit Breaker via disallowing of global opts to substitute BreakDuration = 0, Timeout = GlobalTimeout, }; GivenDiscoveryMetadata(route3, ports3); var configuration = GivenDynamicRouting(new(), route1, route2, route3); var globalOptions = configuration.GlobalConfiguration.QoSOptions = new(new QoSOptions(GlobalExceptions, GlobalBreakMs)) { RouteKeys = ["R2"], }; GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(WithDiscoveryAndPolly); var downstreamUrls = ports1.Union(ports2).Union(ports3).Select(DownstreamUrl).ToArray(); GivenMultipleServiceInstancesAreRunning(downstreamUrls, Enumerable.Repeat(Body(), downstreamUrls.Length).ToArray(), codes: Enumerable.Repeat(HttpStatusCode.NotFound, ports1.Length) .Concat(Enumerable.Repeat(HttpStatusCode.InternalServerError, ports2.Length)) .Concat(Enumerable.Repeat(HttpStatusCode.OK, ports3.Length)) .ToArray()); using var steps = new QosSteps(this); WhenIGetUrlOnTheApiGatewayConcurrently($"/{route1.ServiceName}/", 2); ThenAllStatusCodesShouldBe(HttpStatusCode.NotFound); // QoS is switched off and the scope doesn't matter ThenAllResponseBodiesShouldBe(Body()); steps.CounterStrategy = (port) => { int index = Array.FindIndex(downstreamUrls, url => new Uri(url).Port == port); int count = Interlocked.Increment(ref _counters[index]); }; await steps.TestRouteCircuitBreaker(ports2, $"/{route2.ServiceName}/", globalOptions, isDiscovery: true); // test global scenario await steps.TestRouteTimeout(ports3, $"/{route3.ServiceName}/", route3.QoSOptions); ThenServicesShouldHaveBeenCalledTimes(1, 1, 3, 1, 2, 0); } private FileConfiguration GivenDynamicRouting(Dictionary> services, params FileDynamicRoute[] routes) { var config = new FileConfiguration() { DynamicRoutes = new(routes), GlobalConfiguration = new() { DownstreamScheme = Uri.UriSchemeHttp, ServiceDiscoveryProvider = new() { Type = nameof(DynamicRoutingDiscoveryProvider), Host = "doesn't matter for this provider", // it should not be empty due to DownstreamRouteProviderFactory.Get Port = 1, // see DownstreamRouteProviderFactory.IsServiceDiscovery }, LoadBalancerOptions = new(nameof(RoundRobin)), }, }; config.GlobalConfiguration.Metadata = services.ToDictionary(x => x.Key, x => x.Value.Csv()); return config; } private FileDynamicRoute GivenLbRoute(string serviceName, string serviceNamespace = null, string loadBalancer = null, string key = null) => new() { ServiceName = serviceName, ServiceNamespace = serviceNamespace ?? ServiceNamespace(), LoadBalancerOptions = new(loadBalancer ?? nameof(RoundRobin)), Key = key, }; private static void GivenDiscoveryMetadata(FileDynamicRoute route, int[] ports) => route.Metadata = new Dictionary() { { route.ServiceName, ports.Select(DownstreamUrl).Csv() }, }; private static readonly ServiceDiscoveryFinderDelegate DynamicRoutingDiscoveryFinder = (provider, config, route) => new DynamicRoutingDiscoveryProvider(provider, config, route); private static void WithDiscovery(IServiceCollection services) => services .AddSingleton(DynamicRoutingDiscoveryFinder) .AddOcelot(); private static void WithDiscoveryAndPolly(IServiceCollection services) => services .AddSingleton(DynamicRoutingDiscoveryFinder) .AddOcelot().AddPolly(); private static void WithDiscoveryAndRequesterTesting(IServiceCollection services) { WithDiscovery(services); RequesterSteps.WithRequesterTesting(services, false); } private static Action WithDiscoveryAndJwtBearerAuthentication(AuthenticationSteps steps) { Action ocelotServices = WithDiscovery; void withJwtBearerAuthentication(IServiceCollection services) => steps.WithJwtBearerAuthentication(services, false); ocelotServices += withJwtBearerAuthentication; return ocelotServices; } private void ThenRouteHttpHandlerOptionsAre(string serviceName, IDictionary metadata, int maxConnections, int seconds, bool useTracing) { var pool = OcelotServices.GetService() as TestMessageInvokerPool; pool.ShouldNotBeNull(); var tracer = OcelotServices.GetService() as TestTracer; tracer.ShouldNotBeNull(); foreach (var kv in pool.CreatedHandlers.Where(x => x.Key.ServiceName == serviceName)) { var downstream = kv.Key; var httpHandler = kv.Value; httpHandler.MaxConnectionsPerServer.ShouldBe(maxConnections); httpHandler.PooledConnectionLifetime.TotalSeconds.ShouldBe(seconds); downstream.HttpHandlerOptions.UseTracing.ShouldBe(useTracing); } var csvData = metadata[serviceName]; var serviceUrls = csvData.Split(','); tracer.Requests.Count.ShouldBe(serviceUrls.Length); foreach (var url in serviceUrls) { var request = tracer.Requests.Keys.SingleOrDefault(k => k.RequestUri.AbsoluteUri.StartsWith(url)); (request is not null).ShouldBe(useTracing); } } protected override string ServiceNamespace() => nameof(DynamicRoutingTests); } public class DynamicRoutingDiscoveryProvider : IServiceDiscoveryProvider { private readonly IServiceProvider _serviceProvider; private readonly ServiceProviderConfiguration _config; private readonly DownstreamRoute _downstreamRoute; public DynamicRoutingDiscoveryProvider(IServiceProvider serviceProvider, ServiceProviderConfiguration config, DownstreamRoute downstreamRoute) { _serviceProvider = serviceProvider; _config = config; _downstreamRoute = downstreamRoute; } public Task> GetAsync() { if (!_downstreamRoute.MetadataOptions.Metadata.TryGetValue(_downstreamRoute.ServiceName, out var data) || data.IsEmpty()) return Task.FromResult>(new()); var urls = _downstreamRoute .GetMetadata(_downstreamRoute.ServiceName) .Select(x => new Uri(x)) .ToList(); var services = urls .Select(url => new Service( name: _downstreamRoute.ServiceName, hostAndPort: new(url.Host, url.Port, url.Scheme.IfEmpty(_downstreamRoute.DownstreamScheme)), id: $"{_downstreamRoute.ServiceNamespace}.{_downstreamRoute.ServiceName}", version: DateTime.UtcNow.ToString("O"), tags: Enumerable.Empty())) .ToList(); return Task.FromResult(services); } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/EurekaServiceDiscoveryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.LoadBalancer.Balancers; using Ocelot.Provider.Eureka; using Steeltoe.Common.Discovery; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class EurekaServiceDiscoveryTests : Steps { private readonly List _eurekaInstances; public EurekaServiceDiscoveryTests() { _eurekaInstances = new List(); } #if NET10_0_OR_GREATER [Theory(Skip = "TODO Requires upgrade to v4.0 after package upgraded")] #else [Theory] #endif [Trait("Feat", "262")] // https://github.com/ThreeMammals/Ocelot/issues/262 [InlineData(true)] [InlineData(false)] public async Task Should_use_eureka_service_discovery_and_make_request(bool dotnetRunningInContainer) { Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); var serviceName = "product"; var eurekaPort = 8761; var port = PortFinder.GetRandomPort(); var instanceOne = new FakeEurekaService(serviceName, "localhost", port, false, new Uri(DownstreamUrl(port)), new Dictionary()); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], ServiceName = serviceName, LoadBalancerOptions = new() { Type = nameof(LeastConnection) }, }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new() { Type = nameof(Eureka), }, }, }; GivenEurekaProductServiceOneIsRunning(port, HttpStatusCode.OK); GivenThereIsAFakeEurekaServiceDiscoveryProvider(eurekaPort, serviceName); GivenTheServicesAreRegisteredWithEureka(instanceOne); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunningWithEureka(); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); ThenTheResponseBodyShouldBe(nameof(Should_use_eureka_service_discovery_and_make_request)); } private void GivenOcelotIsRunningWithEureka() => GivenOcelotIsRunning(s => s.AddOcelot().AddEureka()); private void GivenTheServicesAreRegisteredWithEureka(params IServiceInstance[] serviceInstances) { foreach (var instance in serviceInstances) { _eurekaInstances.Add(instance); } } private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(int port, string serviceName) { Task MapEurekaService(HttpContext context) { if (context.Request.Path.Value != "/eureka/apps/") return Task.CompletedTask; var apps = new List(); foreach (var serviceInstance in _eurekaInstances) { var a = new Application { name = serviceName, instance = new List { new() { instanceId = $"{serviceInstance.Host}:{serviceInstance}", hostName = serviceInstance.Host, app = serviceName, ipAddr = "127.0.0.1", status = "UP", overriddenstatus = "UNKNOWN", port = new Port {value = serviceInstance.Port, enabled = "true"}, securePort = new SecurePort {value = serviceInstance.Port, enabled = "true"}, countryId = 1, dataCenterInfo = new DataCenterInfo {value = "com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", name = "MyOwn"}, leaseInfo = new LeaseInfo { renewalIntervalInSecs = 30, durationInSecs = 90, registrationTimestamp = 1457714988223, lastRenewalTimestamp= 1457716158319, evictionTimestamp = 0, serviceUpTimestamp = 1457714988223, }, metadata = new() { value = "java.util.Collections$EmptyMap", }, homePageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", statusPageUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", healthCheckUrl = $"{serviceInstance.Host}:{serviceInstance.Port}", vipAddress = serviceName, isCoordinatingDiscoveryServer = "false", lastUpdatedTimestamp = "1457714988223", lastDirtyTimestamp = "1457714988172", actionType = "ADDED", }, }, }; apps.Add(a); } var applications = new EurekaApplications { applications = new Applications { application = apps, apps__hashcode = "UP_1_", versions__delta = "1", }, }; var json = JsonConvert.SerializeObject(applications); context.Response.Headers.Append("Content-Type", "application/json"); return context.Response.WriteAsync(json); } handler.GivenThereIsAServiceRunningOn(port, MapEurekaService); } private void GivenEurekaProductServiceOneIsRunning(int port, HttpStatusCode statusCode, [CallerMemberName] string responseBody = null) { Task MapStatusAndError(HttpContext context) { try { context.Response.StatusCode = (int)statusCode; return context.Response.WriteAsync(responseBody); } catch (Exception exception) { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; return context.Response.WriteAsync(exception.StackTrace); } } handler.GivenThereIsAServiceRunningOn(port, MapStatusAndError); } } public class FakeEurekaService : IServiceInstance { public FakeEurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) { ServiceId = serviceId; Host = host; Port = port; IsSecure = isSecure; Uri = uri; Metadata = metadata; } public string ServiceId { get; } public string Host { get; } public int Port { get; } public bool IsSecure { get; } public Uri Uri { get; } public IDictionary Metadata { get; } } #pragma warning disable IDE1006 // Naming Styles public class Port { [JsonProperty("$")] public int value { get; set; } [JsonProperty("@enabled")] public string enabled { get; set; } } public class SecurePort { [JsonProperty("$")] public int value { get; set; } [JsonProperty("@enabled")] public string enabled { get; set; } } public class DataCenterInfo { [JsonProperty("@class")] public string value { get; set; } public string name { get; set; } } public class LeaseInfo { public int renewalIntervalInSecs { get; set; } public int durationInSecs { get; set; } public long registrationTimestamp { get; set; } public long lastRenewalTimestamp { get; set; } public int evictionTimestamp { get; set; } public long serviceUpTimestamp { get; set; } } public class ValueMetadata { [JsonProperty("@class")] public string value { get; set; } } public class Instance { public string instanceId { get; set; } public string hostName { get; set; } public string app { get; set; } public string ipAddr { get; set; } public string status { get; set; } public string overriddenstatus { get; set; } public Port port { get; set; } public SecurePort securePort { get; set; } public int countryId { get; set; } public DataCenterInfo dataCenterInfo { get; set; } public LeaseInfo leaseInfo { get; set; } public ValueMetadata metadata { get; set; } public string homePageUrl { get; set; } public string statusPageUrl { get; set; } public string healthCheckUrl { get; set; } public string vipAddress { get; set; } public string isCoordinatingDiscoveryServer { get; set; } public string lastUpdatedTimestamp { get; set; } public string lastDirtyTimestamp { get; set; } public string actionType { get; set; } } public class Application { public string name { get; set; } public List instance { get; set; } } public class Applications { public string versions__delta { get; set; } public string apps__hashcode { get; set; } public List application { get; set; } } public class EurekaApplications { public Applications applications { get; set; } } #pragma warning restore IDE1006 // Naming Styles ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/KubeIntegrationTests.cs ================================================ using KubeClient; using KubeClient.Models; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.ServiceDiscovery; /// /// Contains integration tests. /// Move to integration testing, and add at least one "happy path" unit test. /// // [Collection(nameof(SequentialTests))] public class KubeIntegrationTests : Steps { static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings; private readonly Mock _factory; private readonly Mock _logger; public KubeIntegrationTests() { _factory = new(); _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); } [Fact] [Trait("Feat", "345")] // https://github.com/ThreeMammals/Ocelot/issues/345 [Trait("Release", "13.2.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.2.0 public async Task Should_return_service_from_k8s() { // Arrange var given = GivenClientAndProvider(out var serviceBuilder); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new Service[] { new(nameof(Should_return_service_from_k8s), new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, responseStatusCode: HttpStatusCode.OK, endpoints, out Lazy receivedToken); // Act var services = await given.Provider.GetAsync(); // Assert services.ShouldNotBeNull().Count.ShouldBe(1); receivedToken.Value.ShouldBe($"Bearer {nameof(Should_return_service_from_k8s)}"); } [Theory] [InlineData(HttpStatusCode.BadRequest)] [InlineData(HttpStatusCode.Forbidden)] [InlineData(HttpStatusCode.InternalServerError)] [InlineData(HttpStatusCode.NotFound)] [Trait("PR", "2266")] // https://github.com/ThreeMammals/Ocelot/pull/2266 [Trait("Release", "24.0.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/24.0.0 public async Task Should_not_return_service_from_k8s_when_k8s_api_returns_error_response(HttpStatusCode expectedStatusCode) { // Arrange var given = GivenClientAndProvider(out var serviceBuilder); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new Service[] { new(nameof(Should_not_return_service_from_k8s_when_k8s_api_returns_error_response), new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, expectedStatusCode, endpoints, out Lazy receivedToken); string expectedKubeApiErrorMessage = GetKubeApiErrorMessage(serviceName: given.ProviderOptions.KeyOfServiceInK8s, given.ProviderOptions.KubeNamespace, expectedStatusCode); string expectedLogMessage = $"Failed to retrieve v1/Endpoints '{given.ProviderOptions.KeyOfServiceInK8s}' in namespace '{given.ProviderOptions.KubeNamespace}': (HTTP.{expectedStatusCode}/Failure/{expectedStatusCode}): {expectedKubeApiErrorMessage}"; _logger.Setup(logger => logger.LogError(It.IsAny>(), It.IsAny())) .Callback((Func messageFactory, Exception exception) => { messageFactory.ShouldNotBeNull(); string logMessage = messageFactory(); logMessage.ShouldNotBeNullOrWhiteSpace(); // This is a little fragile, as it may change if other entries are logged due to implementation changes. // Unfortunately, the use of a factory delegate for the log message, combined with reuse of Kube's logger for Retry.OperationAsync makes this tricky to test any other way so this is probably the best we can do for now. if (logMessage.StartsWith("Ocelot Retry strategy")) { return; } logMessage.ShouldBe(expectedLogMessage); exception.ShouldNotBeNull(); KubeApiException kubeApiException = exception.ShouldBeOfType(); StatusV1 errorResponse = kubeApiException.Status; errorResponse.Status.ShouldBe(StatusV1.FailureStatus); errorResponse.Code.ShouldBe((int)expectedStatusCode); errorResponse.Reason.ShouldBe(expectedStatusCode.ToString()); errorResponse.Message.ShouldNotBeNullOrWhiteSpace(); }) .Verifiable($"IOcelotLogger.LogError() was not called."); // Act var services = await given.Provider.GetAsync(); // Assert services.ShouldNotBeNull().Count.ShouldBe(0); receivedToken.Value.ShouldBe($"Bearer {nameof(Should_not_return_service_from_k8s_when_k8s_api_returns_error_response)}"); _logger.Verify(); } [Fact] [Trait("Bug", "2110")] // https://github.com/ThreeMammals/Ocelot/issues/2110 [Trait("Release", "23.3.4")] // https://github.com/ThreeMammals/Ocelot/releases/tag/23.3.4 public async Task Should_return_single_service_from_k8s_during_concurrent_calls() { // Arrange var given = GivenClientAndProvider(out var serviceBuilder); var manualResetEvent = new ManualResetEvent(false); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => { manualResetEvent.WaitOne(); return new Service[] { new(nameof(Should_return_single_service_from_k8s_during_concurrent_calls), new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }; }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy receivedToken); // Act var services = new List(); async Task WhenIGetTheServices() => services = await given.Provider.GetAsync(); var getServiceTasks = Task.WhenAll( WhenIGetTheServices(), WhenIGetTheServices()); manualResetEvent.Set(); await getServiceTasks; // Assert receivedToken.Value.ShouldBe($"Bearer {nameof(Should_return_single_service_from_k8s_during_concurrent_calls)}"); services.ShouldNotBeNull().Count.ShouldBe(1); services.ShouldAllBe(s => s != null); } private (IKubeApiClient Client, KubeClientOptions ClientOptions, Kube Provider, KubeRegistryConfiguration ProviderOptions) GivenClientAndProvider(out Mock serviceBuilder, string namespaces = null, [CallerMemberName] string serviceName = null) { namespaces ??= nameof(KubeIntegrationTests); var kubePort = PortFinder.GetRandomPort(); serviceName ??= "test" + kubePort; var kubeEndpointUrl = $"{Uri.UriSchemeHttp}://localhost:{kubePort}"; var options = new KubeClientOptions { AccessToken = serviceName, // "txpc696iUhbVoudg164r93CxDTrKRVWG", AllowInsecure = true, ApiEndPoint = new Uri(kubeEndpointUrl), AuthStrategy = KubeAuthStrategy.BearerToken, }; IKubeApiClient client = KubeApiClient.Create(options); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = serviceName, KubeNamespace = namespaces, }; serviceBuilder = new(); var provider = new Kube(config, _factory.Object, client, serviceBuilder.Object); return (client, options, provider, config); } protected EndpointsV1 GivenEndpoints( string namespaces = nameof(KubeIntegrationTests), [CallerMemberName] string serviceName = "test") { var endpoints = new EndpointsV1 { Kind = "endpoint", ApiVersion = "1.0", Metadata = new ObjectMetaV1 { Name = serviceName, Namespace = namespaces, }, }; var subset = new EndpointSubsetV1(); subset.Addresses.Add(new EndpointAddressV1 { Ip = "127.0.0.1", Hostname = "localhost", }); subset.Ports.Add(new EndpointPortV1 { Port = 80, }); endpoints.Subsets.Add(subset); return endpoints; } protected void GivenThereIsAFakeKubeServiceDiscoveryProvider( string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy receivedToken) => GivenThereIsAFakeKubeServiceDiscoveryProvider(url, namespaces, serviceName, HttpStatusCode.OK, endpointEntries, out receivedToken); protected void GivenThereIsAFakeKubeServiceDiscoveryProvider(string url, string namespaces, string serviceName, HttpStatusCode responseStatusCode, EndpointsV1 endpointEntries, out Lazy receivedToken) { var token = string.Empty; receivedToken = new(() => token); handler.GivenThereIsAServiceRunningOn(url, ProcessKubernetesRequest); Task ProcessKubernetesRequest(HttpContext context) { if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") { string responseBody; if (context.Request.Headers.TryGetValue("Authorization", out var values)) { token = values.First(); } if (responseStatusCode == HttpStatusCode.OK) { responseBody = JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings); } else { responseBody = JsonConvert.SerializeObject(new StatusV1 { Message = GetKubeApiErrorMessage(serviceName, namespaces, responseStatusCode), Reason = responseStatusCode.ToString(), Code = (int)responseStatusCode, Status = StatusV1.FailureStatus, }, JsonSerializerSettings); } context.Response.StatusCode = (int)responseStatusCode; context.Response.Headers.Append("Content-Type", "application/json"); return context.Response.WriteAsync(responseBody); } return Task.CompletedTask; } } private static string GetKubeApiErrorMessage(string serviceName, string kubeNamespace, HttpStatusCode responseStatusCode) { return $"Failed to retrieve v1/Endpoints '{serviceName}' in namespace '{kubeNamespace}' (HTTP.{responseStatusCode}/Failure/{responseStatusCode}): This is an error response for HTTP status code {(int)responseStatusCode} ('{responseStatusCode}') from the fake Kubernetes API."; } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/KubernetesServiceDiscoveryTests.cs ================================================ using KubeClient; using KubeClient.Models; using KubeClient.ResourceClients; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Newtonsoft.Json; using Ocelot.AcceptanceTests.LoadBalancer; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer.Balancers; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class KubernetesServiceDiscoveryTests : ConcurrentSteps { private readonly string _kubernetesUrl; private string _receivedToken; private readonly Action _kubeClientOptionsConfigure; public KubernetesServiceDiscoveryTests() { _kubernetesUrl = DownstreamUrl(PortFinder.GetRandomPort()); _kubeClientOptionsConfigure = opts => { opts.ApiEndPoint = new Uri(_kubernetesUrl); opts.AccessToken = "txpc696iUhbVoudg164r93CxDTrKRVWG"; opts.AuthStrategy = KubeAuthStrategy.BearerToken; opts.AllowInsecure = true; }; } [Theory] [InlineData(nameof(Kube))] [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304 [InlineData(nameof(WatchKube))] public void ShouldReturnServicesFromK8s(string discoveryType) { var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subsetV1 = GivenSubsetAddress(downstream); var endpoints = GivenEndpoints(subsetV1); var route = GivenRouteWithServiceName(ServiceName()); var configuration = GivenKubeConfiguration(route, discoveryType); string serviceName = ServiceName(), downstreamResponse = serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => GivenWatchReceivedEvent()) .When(_ => WhenIGetUrlOnTheApiGateway("/")) .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe($"1^:^{downstreamResponse}")) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(1)) .And(x => x.ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) .BDDfy(); } [Theory] [Trait("Feat", "1967")] [InlineData("", HttpStatusCode.BadGateway)] [InlineData("http", HttpStatusCode.OK)] public void ShouldReturnServicesByPortNameAsDownstreamScheme(string downstreamScheme, HttpStatusCode statusCode) { const string serviceName = "example-web"; var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subsetV1 = GivenSubsetAddress(downstream); // Ports[0] -> port(https, 443) // Ports[1] -> port(http, not 80) subsetV1.Ports.Insert(0, new() { Name = "https", // This service instance is offline -> BadGateway Port = 443, }); var endpoints = GivenEndpoints(subsetV1); var route = GivenRouteWithServiceName(); route.DownstreamPathTemplate = "/{url}"; route.DownstreamScheme = downstreamScheme; // !!! Warning !!! Select port by name as scheme route.UpstreamPathTemplate = "/api/example/{url}"; route.ServiceName = serviceName; // "example-web" var configuration = GivenKubeConfiguration(route, nameof(Kube)); this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, nameof(ShouldReturnServicesByPortNameAsDownstreamScheme))) .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => WhenIGetUrlOnTheApiGateway("/api/example/1")) .Then(_ => ThenTheStatusCodeShouldBe(statusCode)) .And(_ => ThenTheResponseBodyShouldBe(downstreamScheme == "http" ? "1^:^" + nameof(ShouldReturnServicesByPortNameAsDownstreamScheme) : string.Empty)) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(downstreamScheme == "http" ? 1 : 0)) .And(x => x.ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) .BDDfy(); } [Theory] [Trait("Bug", "2110")] [InlineData(1, 30, null)] [InlineData(2, 50, null)] [InlineData(3, 50, null)] [InlineData(4, 50, null)] [InlineData(5, 50, null)] [InlineData(6, 99, null)] [InlineData(7, 99, null)] [InlineData(8, 99, null)] [InlineData(9, 999, null)] [InlineData(10, 999, nameof(Kube))] // [InlineData(10, 999, nameof(PollKube))] [InlineData(10, 999, nameof(WatchKube))] public void ShouldHighlyLoadOnStableKubeProvider_WithRoundRobinLoadBalancing(int totalServices, int totalRequests, string discoveryType) { // Skip in MacOS because the test is very unstable if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // the test is stable in Linux and Windows only return; discoveryType ??= nameof(Kube); int zeroGeneration = 0, k8sCount = totalRequests; int bottom = totalRequests / totalServices, top = totalRequests - (bottom * totalServices) + bottom; var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices, discoveryType); GivenThereIsAFakeKubernetesProvider(endpoints); // stable, services will not be removed from the list HighlyLoadOnKubeProviderAndRoundRobinBalancer(discoveryType, totalRequests, zeroGeneration, k8sCount); if (discoveryType == nameof(PollKube)) return; // TODO ThenAllServicesCalledRealisticAmountOfTimes(bottom, top); ThenServiceCountersShouldMatchLeasingCounters(_roundRobinAnalyzer, servicePorts, totalRequests); } [Theory] [Trait("Bug", "2110")] [InlineData(5, 50, 1, null)] [InlineData(5, 50, 2, null)] [InlineData(5, 50, 3, null)] [InlineData(5, 50, 4, nameof(Kube))] // [InlineData(5, 50, 4, nameof(PollKube))] [InlineData(5, 50, 4, nameof(WatchKube))] public void ShouldHighlyLoadOnUnstableKubeProvider_WithRoundRobinLoadBalancing(int totalServices, int totalRequests, int k8sGeneration, string discoveryType) { // Skip in MacOS because the test is very unstable if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // the test is stable in Linux and Windows only return; discoveryType ??= nameof(Kube); int failPerThreads = (totalRequests / k8sGeneration) - 1, // k8sGeneration means number of offline services k8sCount = totalRequests; var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing(totalServices, discoveryType); GivenThereIsAFakeKubernetesProvider(endpoints, false, k8sGeneration, failPerThreads); // false means unstable, k8sGeneration services will be removed from the list HighlyLoadOnKubeProviderAndRoundRobinBalancer(discoveryType, totalRequests, discoveryType == nameof(WatchKube) ? 0 : k8sGeneration, k8sCount); ThenAllServicesCalledOptimisticAmountOfTimes(_roundRobinAnalyzer); // with unstable checkings ThenServiceCountersShouldMatchLeasingCounters(_roundRobinAnalyzer, servicePorts, totalRequests); } [Theory] [InlineData(nameof(Kube))] [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304 [InlineData(nameof(WatchKube))] [Trait("Feat", "2256")] public void ShouldReturnServicesFromK8s_AddKubernetesWithNullConfigureOptions(string discoveryType) { var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subsetV1 = GivenSubsetAddress(downstream); var endpoints = GivenEndpoints(subsetV1); var route = GivenRouteWithServiceName(); var configuration = GivenKubeConfiguration(route, discoveryType, "txpc696iUhbVoudg164r93CxDTrKRVWG"); string serviceName = ServiceName(), downstreamResponse = serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) .And(x => x.GivenThereIsAFakeKubernetesProvider(endpoints, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(AddKubernetesWithNullConfigureOptions)) .When(_ => GivenWatchReceivedEvent()) .When(_ => WhenIGetUrlOnTheApiGateway("/")) .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe($"1^:^{downstreamResponse}")) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(1)) .And(x => x.ThenTheTokenIs("Bearer txpc696iUhbVoudg164r93CxDTrKRVWG")) .BDDfy(); } [Fact] [Trait("Feat", "2168")] [Trait("PR", "2174")] // https://github.com/ThreeMammals/Ocelot/pull/2174 public void ShouldReturnServicesFromK8s_OneWatchRequestUpdatesServicesInfo() { (EndpointsV1 endpoints, string downstreamUrl) = GetServiceInstance(); (EndpointsV1 updatedEndpoints, string updateDownstreamUrl) = GetServiceInstance(); ResourceEventV1[] events = [ new() { EventType = ResourceEventType.Added, Resource = endpoints }, new() { EventType = ResourceEventType.Modified, Resource = updatedEndpoints } ]; var route = GivenRouteWithServiceName(); var configuration = GivenKubeConfiguration(route, nameof(WatchKube)); string serviceName = ServiceName(), downstreamResponse = serviceName; var updatedDownstreamResponse = "updated_content" + serviceName; this.Given(x => GivenServiceInstanceIsRunning(downstreamUrl, downstreamResponse)) .Given(x => GivenServiceInstanceIsRunning(updateDownstreamUrl, updatedDownstreamResponse)) .And(x => x.GivenThereIsAFakeKubernetesProvider(events, serviceName)) .And(_ => GivenThereIsAConfiguration(configuration)) .And(_ => GivenOcelotIsRunning(WithKubernetes)) .When(_ => GivenWatchReceivedEvent()) .When(_ => WhenIGetUrlOnTheApiGatewayConcurrently("/", 10)) .Then(_ => ThenAllStatusCodesShouldBe(HttpStatusCode.OK)) .Then(_ => ThenAllResponseBodiesShouldBe(downstreamResponse)) .And(_ => ThenK8sShouldBeCalledExactly(1)) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(10)) .When(_ => GivenWatchReceivedEvent()) .Given(_ => GivenIWaitAsync(100)) .When(_ => WhenIGetUrlOnTheApiGatewayConcurrently("/", 10)) .Then(_ => ThenAllStatusCodesShouldBe(HttpStatusCode.OK)) .Then(_ => ThenAllResponseBodiesShouldBe(updatedDownstreamResponse)) .And(_ => ThenK8sShouldBeCalledExactly(1)) .And(x => ThenAllServicesShouldHaveBeenCalledTimes(20)) .BDDfy(); (EndpointsV1 Endpoints, string DownstreamUrl) GetServiceInstance() { var servicePort = PortFinder.GetRandomPort(); var downstreamUrl = LoopbackLocalhostUrl(servicePort); var downstream = new Uri(downstreamUrl); var subset = GivenSubsetAddress(downstream); var endpoints = GivenEndpoints(subset); return (endpoints, downstreamUrl); } } [Theory] [Trait("Feat", "585")] [Trait("Feat", "2319")] [Trait("PR", "2324")] // https://github.com/ThreeMammals/Ocelot/pull/2324 [InlineData(nameof(Kube))] // [InlineData(nameof(PollKube))] // Bug 2304 -> https://github.com/ThreeMammals/Ocelot/issues/2304 [InlineData(nameof(WatchKube))] public void ShouldApplyGlobalLoadBalancerOptions_ForAllDynamicRoutes(string discoveryType) { static void ConfigureDynamicRouting(FileConfiguration configuration) { configuration.GlobalConfiguration.LoadBalancerOptions = new(nameof(RoundRobin)); configuration.GlobalConfiguration.DownstreamScheme = Uri.UriSchemeHttp; configuration.Routes = []; // dynamic routing configuration.DynamicRoutes = []; // no dynamic routes, for ALL dynamic routes } var (endpoints, servicePorts) = GivenServiceDiscoveryAndLoadBalancing( 5, discoveryType, nameof(RoundRobin), ConfigureDynamicRouting, WithKubernetesAndFakeKubeServiceCreator); GivenThereIsAFakeKubernetesProvider(endpoints); if (discoveryType == nameof(WatchKube)) GivenWatchReceivedEvent(); var upstreamPath = $"/{ServiceNamespace()}.{ServiceName()}/"; WhenIGetUrlOnTheApiGatewayConcurrently(upstreamPath, 50); if (discoveryType == nameof(PollKube)) { //#if NET10_0_OR_GREATER _k8sCounter.ShouldBeLessThan(50); //#else // if (IsCiCd()) _k8sCounter.ShouldBeInRange(48, 52); // else _k8sCounter.ShouldBeGreaterThanOrEqualTo(50); // can be 50, 51 and sometimes 52 //#endif } else { _k8sCounter.ShouldBe(discoveryType == nameof(WatchKube) ? 1 : 50); } _k8sServiceGeneration.ShouldBe(0); ThenAllStatusCodesShouldBe(HttpStatusCode.OK); ThenAllServicesShouldHaveBeenCalledTimes(50); ThenAllServicesCalledRealisticAmountOfTimes(9, 11); // soft assertion ThenServicesShouldHaveBeenCalledTimes(10, 10, 10, 10, 10); // distribution by RoundRobin algorithm, aka strict assertion } private void AddKubernetesWithNullConfigureOptions(IServiceCollection services) => services.AddOcelot().AddKubernetes(configureOptions: null); private (EndpointsV1 Endpoints, int[] ServicePorts) GivenServiceDiscoveryAndLoadBalancing( int totalServices, string discoveryType = nameof(Kube), string loadBalancerType = nameof(RoundRobinAnalyzer), Action configure = null, Action services = null, [CallerMemberName] string serviceName = null) { serviceName ??= ServiceName(); var servicePorts = PortFinder.GetPorts(totalServices); var downstreamUrls = servicePorts .Select(port => LoopbackLocalhostUrl(port, Array.IndexOf(servicePorts, port))) // TODO Develop thread-safe version of the method .ToArray(); // based on localhost aka loopback network interface var downstreams = downstreamUrls.Select(url => new Uri(url)) .ToList(); var downstreamResponses = downstreams .Select(ds => $"{serviceName}:{ds.Host}:{ds.Port}") .ToArray(); var subset = new EndpointSubsetV1(); downstreams.ForEach(ds => GivenSubsetAddress(ds, subset)); var endpoints = GivenEndpoints(subset, serviceName); // totalServices service instances with different ports var route = GivenRouteWithServiceName(serviceName, loadBalancerType); // !!! var configuration = GivenKubeConfiguration(route, discoveryType.IfEmpty(nameof(Kube))); configure?.Invoke(configuration); GivenMultipleServiceInstancesAreRunning(downstreamUrls, downstreamResponses); GivenThereIsAConfiguration(configuration); int ocPort = GivenOcelotIsRunning(services ?? WithKubernetesAndRoundRobin); return (endpoints, servicePorts); } private void HighlyLoadOnKubeProviderAndRoundRobinBalancer(string discoveryType, int totalRequests, int k8sGenerationNo, int? k8sCount = null) { if (discoveryType == nameof(WatchKube)) { k8sCount = GivenWatchReceivedEvent(); // 1 k8sGenerationNo = 0; } // Act WhenIGetUrlOnTheApiGatewayConcurrently("/", totalRequests); // load by X parallel requests // Assert int count = k8sCount ?? totalRequests; if (discoveryType == nameof(WatchKube)) _k8sCounter.ShouldBeLessThanOrEqualTo(count); // TODO This is something abnormal due to values 997-999, but actual value should be 1. Need to double check this. if (discoveryType == nameof(PollKube)) { //#if NET10_0_OR_GREATER //_k8sCounter.ShouldBeLessThanOrEqualTo(count); _k8sCounter.ShouldBeLessThan(count); //#else // if (IsCiCd()) _k8sCounter.ShouldBeInRange(count - 1, count + 1); // else _k8sCounter.ShouldBeGreaterThanOrEqualTo(count); // can be 50, 51 and sometimes 52 //#endif } else _k8sCounter.ShouldBeGreaterThanOrEqualTo(count); // integration endpoint called times _k8sServiceGeneration.ShouldBe(k8sGenerationNo); #if NET10_0_OR_GREATER if (discoveryType == nameof(PollKube)) _responses.Count(x => x.Value.StatusCode == HttpStatusCode.OK) .ShouldBeGreaterThan(_responses.Count(x => x.Value.StatusCode != HttpStatusCode.OK)); else ThenAllStatusCodesShouldBe(HttpStatusCode.OK); #else ThenAllStatusCodesShouldBe(HttpStatusCode.OK); #endif #if NET10_0_OR_GREATER if (discoveryType == nameof(PollKube)) _counters.Sum().ShouldBeLessThanOrEqualTo(totalRequests, CalledTimesMessage()); else ThenAllServicesShouldHaveBeenCalledTimes(totalRequests); #else ThenAllServicesShouldHaveBeenCalledTimes(totalRequests); #endif _roundRobinAnalyzer.ShouldNotBeNull().Analyze(); #if NET10_0_OR_GREATER if (discoveryType == nameof(PollKube)) _roundRobinAnalyzer.Events.Count.ShouldBeInRange((int)(0.9 * totalRequests), totalRequests); else _roundRobinAnalyzer.Events.Count.ShouldBe(totalRequests); #else _roundRobinAnalyzer.Events.Count.ShouldBe(totalRequests); #endif _roundRobinAnalyzer.HasManyServiceGenerations(k8sGenerationNo).ShouldBeTrue(); } private void ThenTheTokenIs(string token) => _receivedToken.ShouldBe(token); private void ThenK8sShouldBeCalledExactly(int totalRequests) => _k8sCounter.ShouldBe(totalRequests); private EndpointsV1 GivenEndpoints(EndpointSubsetV1 subset, [CallerMemberName] string serviceName = "") { var e = new EndpointsV1() { Kind = "endpoint", ApiVersion = "1.0", Metadata = new() { Name = serviceName, Namespace = ServiceNamespace(), }, }; e.Subsets.Add(subset); return e; } private static EndpointSubsetV1 GivenSubsetAddress(Uri downstream, EndpointSubsetV1 subset = null) { subset ??= new(); subset.Addresses.Add(new() { Ip = Dns.GetHostAddresses(downstream.Host).Select(x => x.ToString()).First(a => a.Contains('.')), // 127.0.0.1 Hostname = downstream.Host, }); subset.Ports.Add(new() { Name = downstream.Scheme, Port = downstream.Port, }); return subset; } private FileRoute GivenRouteWithServiceName([CallerMemberName] string serviceName = null, string loadBalancerType = nameof(LeastConnection)) => new() { DownstreamPathTemplate = "/", DownstreamScheme = null, // the scheme should not be defined in service discovery scenarios by default, only ServiceName UpstreamPathTemplate = "/", UpstreamHttpMethod = [HttpMethods.Get], ServiceName = serviceName, // !!! ServiceNamespace = ServiceNamespace(), LoadBalancerOptions = new() { Type = loadBalancerType }, }; private FileConfiguration GivenKubeConfiguration(FileRoute route, string type, string token = null) { var u = new Uri(_kubernetesUrl); var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = u.Scheme, Host = u.Host, Port = u.Port, Type = type, PollingInterval = 5 * MaxKubernetesDelay, // 3ms is very fast polling, make sense for PollKube provider only Namespace = ServiceNamespace(), Token = token ?? "Test", }; return configuration; } private const int MaxKubernetesDelay = 10; // ms private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints, [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests)) => GivenThereIsAFakeKubernetesProvider(endpoints, true, 0, 0, serviceName, ServiceNamespace()); private void GivenThereIsAFakeKubernetesProvider(EndpointsV1 endpoints, bool isStable, int offlineServicesNo, int offlinePerThreads, [CallerMemberName] string serviceName = null, string namespaces = null) { _k8sCounter = 0; serviceName ??= ServiceName(); namespaces ??= ServiceNamespace(); handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, async context => { await Task.Delay(Random.Shared.Next(1, MaxKubernetesDelay)); // emulate integration delay up to 10 milliseconds if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") { string json; lock (K8sCounterLocker) { _k8sCounter++; var subset = endpoints.Subsets[0]; // Each offlinePerThreads-th request to integrated K8s endpoint should fail if (!isStable && _k8sCounter % offlinePerThreads == 0 && _k8sCounter >= offlinePerThreads) { while (offlineServicesNo-- > 0) { int index = subset.Addresses.Count - 1; // Random.Shared.Next(0, subset.Addresses.Count - 1); subset.Addresses.RemoveAt(index); subset.Ports.RemoveAt(index); } _k8sServiceGeneration++; } endpoints.Metadata.Generation = _k8sServiceGeneration; json = JsonConvert.SerializeObject(endpoints, KubeResourceClient.SerializerSettings); } if (context.Request.Headers.TryGetValue("Authorization", out var values)) { _receivedToken = values.First(); } context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(json); } await GivenHandleWatchRequest(context, [new() { EventType = ResourceEventType.Added, Resource = endpoints }], namespaces, serviceName); }); } private void GivenThereIsAFakeKubernetesProvider(ResourceEventV1[] events, [CallerMemberName] string serviceName = nameof(KubernetesServiceDiscoveryTests)) { _k8sCounter = 0; var namespaces = ServiceNamespace(); handler.GivenThereIsAServiceRunningOn(_kubernetesUrl, (c) => GivenHandleWatchRequest(c, events, namespaces, serviceName)); } private int GivenWatchReceivedEvent() => _k8sWatchResetEvent.Set() ? 1 : 0; private async Task GivenHandleWatchRequest(HttpContext context, IEnumerable> events, string namespaces, string serviceName) { await Task.Delay(Random.Shared.Next(1, 10)); // emulate integration delay up to 10 milliseconds if (context.Request.Path.Value == $"/api/v1/watch/namespaces/{namespaces}/endpoints/{serviceName}") { _k8sCounter++; if (context.Request.Headers.TryGetValue("Authorization", out var values)) { _receivedToken = values.First(); } context.Response.StatusCode = 200; context.Response.Headers.Append("Content-Type", "application/json"); foreach (var @event in events) { _k8sWatchResetEvent.WaitOne(); var json = JsonConvert.SerializeObject(@event, KubeResourceClient.SerializerSettings); await using var sw = new StreamWriter(context.Response.Body); await sw.WriteLineAsync(json); await sw.FlushAsync(); _k8sWatchResetEvent.Reset(); } // keeping open connection like kube api will slow down tests } } private static ServiceDescriptor GetValidateScopesDescriptor() => ServiceDescriptor.Singleton>( new DefaultServiceProviderFactory(new() { ValidateScopes = true })); private IOcelotBuilder AddKubernetes(IServiceCollection services) => services .Replace(GetValidateScopesDescriptor()) .AddOcelot().AddKubernetes(_kubeClientOptionsConfigure); private void WithKubernetes(IServiceCollection services) => AddKubernetes(services); private void WithKubernetesAndFakeKubeServiceCreator(IServiceCollection services) => AddKubernetes(services) .Services.RemoveAll().AddSingleton(); private void WithKubernetesAndRoundRobin(IServiceCollection services) => AddKubernetes(services) .AddCustomLoadBalancer(GetRoundRobinAnalyzer) .Services.RemoveAll().AddSingleton(); private int _k8sCounter, _k8sServiceGeneration; #if NET9_0_OR_GREATER private static readonly Lock K8sCounterLocker = new(); #else private static readonly object K8sCounterLocker = new(); #endif private RoundRobinAnalyzer _roundRobinAnalyzer; private AutoResetEvent _k8sWatchResetEvent = new(false); private RoundRobinAnalyzer GetRoundRobinAnalyzer(DownstreamRoute route, IServiceDiscoveryProvider provider) { lock (K8sCounterLocker) { return _roundRobinAnalyzer ??= new RoundRobinAnalyzerCreator().Create(route, provider)?.Data as RoundRobinAnalyzer; //??= new RoundRobinAnalyzer(provider.GetAsync, route.ServiceName); } } protected override string ServiceName([CallerMemberName] string serviceName = null) => serviceName ?? nameof(KubernetesServiceDiscoveryTests); protected override string ServiceNamespace() => nameof(KubernetesServiceDiscoveryTests); } internal class FakeKubeServiceCreator : KubeServiceCreator { public FakeKubeServiceCreator(IOcelotLoggerFactory factory) : base(factory) { } protected override ServiceHostAndPort GetServiceHostAndPort(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) { var ports = subset.Ports; var index = subset.Addresses.IndexOf(address); var portV1 = ports[index]; Logger.LogDebug(() => $"K8s service with key '{configuration.KeyOfServiceInK8s}' and address {address.Ip}; Detected port is {portV1.Name}:{portV1.Port}. Total {ports.Count} ports of [{string.Join(',', ports.Select(p => p.Name))}]."); return new ServiceHostAndPort(address.Ip, (int)portV1.Port, portV1.Name); } protected override IEnumerable GetServiceTags(KubeRegistryConfiguration configuration, EndpointsV1 endpoint, EndpointSubsetV1 subset, EndpointAddressV1 address) { var tags = base.GetServiceTags(configuration, endpoint, subset, address) .ToList(); long gen = endpoint.Metadata.Generation ?? 0L; tags.Add($"{nameof(endpoint.Metadata.Generation)}:{gen}"); return tags; } } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/PollKubeConcurrencyIntegrationTests.cs ================================================ using KubeClient; using KubeClient.Models; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.ServiceDiscovery; /// /// Concurrency integration tests for the PollKube service discovery provider. /// /// Tests verify that the handler (Kubernetes API mock) is called the correct number of times based on polling intervals and concurrent request patterns. /// /// The key metric is the handler counter which increments every time the Kubernetes API endpoint is called to fetch service endpoints. /// /// public class PollKubeConcurrencyIntegrationTests : Steps { static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings; private readonly Mock _factory; private readonly Mock _logger; // Handler counter - tracks how many times the fake Kubernetes API was called private int _kubeHandlerCallCount; #if NET9_0_OR_GREATER private static readonly Lock _counterLock = new(); #else private static readonly object _counterLock = new(); #endif private const int PollingInterval = 100; // milliseconds private const int FirstPollingWaitTime = 50; // milliseconds - less than polling interval private const int SecondPollingWaitTime = 150; // milliseconds - slightly more than polling interval public PollKubeConcurrencyIntegrationTests() { _factory = new(); _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _kubeHandlerCallCount = 0; } /// /// Scenario 1: Single call after start - Handler called once (counter = 1) /// /// Arrange: Create PollKube provider /// Act: Make single GetAsync() call /// Assert: Handler counter should be 1 (cold start poll) /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario1")] [Trait("Feature", "PollingBehavior")] public async Task Scenario_1_SingleCallAfterStart_HandlerCalledOnce() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var expectedService = new Service("service-1", new("localhost", 8080), string.Empty, string.Empty, Array.Empty()); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new[] { expectedService }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act var services = await given.Provider.GetAsync(); // Assert services.ShouldNotBeNull(); services.Count.ShouldBe(1); _kubeHandlerCallCount.ShouldBe(1, "Handler should be called exactly once for cold start"); } /// /// Scenario 2: Three parallel calls within polling interval (before 2nd polling) /// Handler called once (counter = 1) /// /// Multiple parallel calls should return the same queued service version without triggering additional polls. /// All three calls occur before the next polling interval elapses. /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario2")] [Trait("Feature", "ParallelCalls")] public async Task Scenario_2_ThreeParallelCallsWithinFirstInterval_HandlerCalledOnce() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 1; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => new Service[] { new($"service-v{version}", new("localhost", 8080), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act // Make initial call to populate queue (cold start - this calls handler once) var firstCall = await given.Provider.GetAsync(); firstCall.ShouldNotBeNull().ShouldNotBeEmpty(); firstCall[0].Name.ShouldBe("service-v1"); _kubeHandlerCallCount.ShouldBe(1, "First call should trigger cold start poll"); // Make three parallel calls within the first polling interval (before 2nd polling) await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken); // Wait less than polling interval var parallelTasks = Enumerable.Range(0, 3) .Select(_ => given.Provider.GetAsync()) .ToArray(); var results = await Task.WhenAll(parallelTasks); // Assert // All three parallel calls should return the same version results.ShouldAllBe(r => r != null && r.Count == 1); results.ShouldAllBe(r => r[0].Name == "service-v1"); // Handler should still be called only once - no additional polling occurred yet _kubeHandlerCallCount.ShouldBe(1, "Handler should not be called again - all parallel calls returned queued service"); } /// /// Scenario 3: Multiple calls in 1st interval, 2nd polling returns new version /// Handler called twice (counter = 2) /// /// First polling interval: multiple calls return version 1 /// Second polling interval: handler is called again, returns version 2 /// Then multiple calls return version 2 /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario3")] [Trait("Feature", "PollingIntervals")] public async Task Scenario_3_FirstIntervalThenSecondPollingWithNewVersion_HandlerCalledTwice() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 1; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => new Service[] { new($"service-v{version}", new("localhost", 8000 + version), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act - First interval: make multiple calls var firstCall = await given.Provider.GetAsync(); firstCall.ShouldNotBeNull(); firstCall[0].Name.ShouldBe("service-v1"); firstCall[0].HostAndPort.DownstreamPort.ShouldBe(8001); _kubeHandlerCallCount.ShouldBe(1); // Multiple calls within first interval await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken); var parallelCalls1 = await Task.WhenAll( Enumerable.Range(0, 3) .Select(_ => given.Provider.GetAsync()) .ToArray()); parallelCalls1.ShouldAllBe(r => r[0].Name == "service-v1"); _kubeHandlerCallCount.ShouldBe(1, "No additional polling yet"); // Act - Wait for 2nd polling interval to occur await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); // Wait more than polling interval version = 2; // Simulate version update // Multiple calls in 2nd interval - should trigger second poll and return new version var secondIntervalCalls = await Task.WhenAll( Enumerable.Range(0, 3) .Select(_ => given.Provider.GetAsync()) .ToArray()); // Assert secondIntervalCalls.ShouldAllBe(r => r[0].Name == "service-v2"); secondIntervalCalls.ShouldAllBe(r => r[0].HostAndPort.DownstreamPort == 8002); // Handler should have been called during the 2nd polling interval _kubeHandlerCallCount.ShouldBe(2, "2nd polling should have called handler"); } /// /// Scenario 4: Multiple calls in each interval, polling returns new version each time /// Handler called during each polling, counter increased by 1 for each poll /// /// This tests that polling happens at regular intervals and each poll increments the counter. /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario4")] [Trait("Feature", "RegularPolling")] public async Task Scenario_4_MultipleCallsEachIntervalNewVersionEachPolling_CounterIncreasedByOne() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 1; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => new Service[] { new($"service-v{version}", new("localhost", 9000 + version), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act & Assert - Interval 1 var call1 = await given.Provider.GetAsync(); call1[0].Name.ShouldBe("service-v1"); _kubeHandlerCallCount.ShouldBe(1); // Parallel calls in interval 1 await Task.Delay(FirstPollingWaitTime, Xunit.TestContext.Current.CancellationToken); var interval1Calls = await Task.WhenAll( Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray()); interval1Calls.ShouldAllBe(r => r[0].Name == "service-v1"); _kubeHandlerCallCount.ShouldBe(1); // Act & Assert - Interval 2 await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); version = 2; var interval2Calls = await Task.WhenAll( Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray()); interval2Calls.ShouldAllBe(r => r[0].Name == "service-v2"); _kubeHandlerCallCount.ShouldBe(2, "Counter should be 2 after 2nd polling"); // Act & Assert - Interval 3 await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); version = 3; var interval3Calls = await Task.WhenAll( Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray()); interval3Calls.ShouldAllBe(r => r[0].Name == "service-v3"); _kubeHandlerCallCount.ShouldBe(3, "Counter should be 3 after 3rd polling"); // Act & Assert - Interval 4 await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); version = 4; var interval4Calls = await Task.WhenAll( Enumerable.Range(0, 2).Select(_ => given.Provider.GetAsync()).ToArray()); interval4Calls.ShouldAllBe(r => r[0].Name == "service-v4"); _kubeHandlerCallCount.ShouldBe(4, "Counter should be 4 after 4th polling"); } /// /// Scenario 5: Heavy load (~1000 concurrent calls) during each polling interval /// Handler called twice (counter = 2) /// /// Heavy load doesn't impact polling which happens at regular intervals. /// Even with 1000 concurrent calls, the handler should be called exactly twice /// (once for cold start, once for 2nd polling interval). /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario5")] [Trait("Feature", "HeavyLoad")] [Trait("Performance", "Stress")] public async Task Scenario_5_HeavyLoad1000CallsEachInterval_HandlerCalledTwice() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 1; var callCount = 0; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => { Interlocked.Increment(ref callCount); return new Service[] { new($"service-v{version}", new("localhost", 7000 + version), string.Empty, string.Empty, Array.Empty()) }; }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act - Cold start with heavy load var coldStartHeavyLoad = await Task.WhenAll( Enumerable.Range(0, 1000) .Select(_ => given.Provider.GetAsync()) .ToArray()); // Assert - Cold start coldStartHeavyLoad.ShouldAllBe(r => r != null && r.Count == 1); // With heavy load, multiple threads might poll, but ideally only once var coldStartCount = _kubeHandlerCallCount; coldStartCount.ShouldBe(1, "Cold start should call handler once despite 1000 concurrent calls"); // Act - Wait for 2nd polling interval with heavy load await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); version = 2; var secondIntervalHeavyLoad = await Task.WhenAll( Enumerable.Range(0, 1000) .Select(_ => given.Provider.GetAsync()) .ToArray()); // Assert - 2nd polling secondIntervalHeavyLoad.ShouldAllBe(r => r != null && r.Count == 1); secondIntervalHeavyLoad.ShouldAllBe(r => r[0].Name == "service-v2"); // Handler should be called exactly twice despite the heavy load _kubeHandlerCallCount.ShouldBe(2, "Handler should be called exactly twice (cold start + 1st polling) despite 1000 concurrent calls per interval"); } /// /// Extended Scenario 5b: Verify all responses are consistent during heavy load /// /// Even under heavy load with 1000+ concurrent calls, all responses should be identical /// until the polling interval updates the services. /// [Fact(Skip = "Under development")] [Trait("Concurrency", "Scenario5Extended")] [Trait("Feature", "ConsistencyUnderLoad")] public async Task Scenario_5b_HeavyLoadAllResponsesConsistent_NoPartialUpdates() { // Arrange _kubeHandlerCallCount = 0; var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 1; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => new Service[] { new($"service-v{version}", new("localhost", 6000 + version), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpointsWithVersion(1); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, endpoints, out Lazy _); // Act - Cold start var coldStartResponses = await Task.WhenAll( Enumerable.Range(0, 500) .Select(_ => given.Provider.GetAsync()) .ToArray()); // Assert - All responses should be for version 1 var v1Count = coldStartResponses.Count(r => r[0].Name == "service-v1"); v1Count.ShouldBe(500, "All cold start responses should be version 1"); _kubeHandlerCallCount.ShouldBe(1); // Act - Wait and trigger 2nd polling await Task.Delay(SecondPollingWaitTime, Xunit.TestContext.Current.CancellationToken); version = 2; // Heavy load that might span polling interval boundary var heavyLoadResponses = await Task.WhenAll( Enumerable.Range(0, 1500) .Select(async (i) => { if (i % 500 == 0) await Task.Delay(10); // Small delay to allow polling to happen return await given.Provider.GetAsync(); }) .ToArray()); // Assert - All responses should be version 1 (queued before 2nd polling) // or version 2 (after 2nd polling), not a mix var v2Count = heavyLoadResponses.Count(r => r[0].Name == "service-v2"); var allV1 = heavyLoadResponses.All(r => r[0].Name == "service-v1"); var allV2 = heavyLoadResponses.All(r => r[0].Name == "service-v2"); (allV1 || allV2).ShouldBeTrue("All responses should be consistent - either all v1 or all v2"); // Handler should be called twice total _kubeHandlerCallCount.ShouldBe(2, "Should have exactly 2 handler calls"); } #region Helper Methods private (IKubeApiClient Client, KubeClientOptions ClientOptions, PollKube Provider, KubeRegistryConfiguration ProviderOptions) GivenClientAndPollKubeProvider(out Mock serviceBuilder, int pollingInterval = PollingInterval, [CallerMemberName] string serviceName = null) { serviceName ??= serviceName; var kubePort = PortFinder.GetRandomPort(); var options = new KubeClientOptions { AccessToken = serviceName, // "txpc696iUhbVoudg164r93CxDTrKRVWG", AllowInsecure = true, ApiEndPoint = new(DownstreamUrl(kubePort)), AuthStrategy = KubeAuthStrategy.BearerToken, }; IKubeApiClient client = KubeApiClient.Create(options); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = serviceName, KubeNamespace = nameof(PollKubeConcurrencyIntegrationTests), }; serviceBuilder = new(); var kubeProvider = new Kube(config, _factory.Object, client, serviceBuilder.Object); var provider = new PollKube(pollingInterval, _factory.Object, kubeProvider); return (client, options, provider, config); } protected void GivenThereIsAFakeKubeServiceDiscoveryProvider( string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy receivedToken) { var token = string.Empty; receivedToken = new(() => token); handler.GivenThereIsAServiceRunningOn(url, ProcessKubernetesRequest); Task ProcessKubernetesRequest(HttpContext context) { if (context.Request.Path.Value == $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") { // Increment handler call counter - this is the key metric being tested lock (_counterLock) { _kubeHandlerCallCount++; } if (context.Request.Headers.TryGetValue("Authorization", out var values)) { token = values.First(); } var responseBody = JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings); context.Response.StatusCode = (int)HttpStatusCode.OK; context.Response.Headers.Append("Content-Type", "application/json"); return context.Response.WriteAsync(responseBody); } return Task.CompletedTask; } } private static EndpointsV1 GivenEndpointsWithVersion(int version) { var endpoints = new EndpointsV1 { Metadata = new ObjectMetaV1 { Name = "test-endpoints", Namespace = nameof(PollKubeConcurrencyIntegrationTests), Generation = version, }, }; var subset = new EndpointSubsetV1(); endpoints.Subsets.Add(subset); subset.Addresses.Add(new() { Ip = "127.0.0.1", TargetRef = new ObjectReferenceV1 { Name = "pod-1" }, }); subset.Ports.Add(new() { Name = "http", Port = 8080, }); return endpoints; } #endregion } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/PollKubeIntegrationTests.cs ================================================ using KubeClient; using KubeClient.Models; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; using System.Runtime.CompilerServices; namespace Ocelot.AcceptanceTests.ServiceDiscovery; /// /// Integration tests for the service discovery provider. /// polls the Kubernetes API at specified intervals to discover services. /// [Trait("Milestone", ".NET 10")] // https://github.com/ThreeMammals/Ocelot/milestone/13 [Trait("Release", "25.0.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/25.0.0 public class PollKubeIntegrationTests : Steps { static JsonSerializerSettings JsonSerializerSettings => KubeClient.ResourceClients.KubeResourceClient.SerializerSettings; private readonly Mock _factory; private readonly Mock _logger; private const int PollingInterval = 100; // milliseconds public PollKubeIntegrationTests() { _factory = new(); _logger = new(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); } [Fact(Skip = "Under development")] [Trait("Feature", "Polling")] public async Task Should_return_service_from_k8s_on_first_call() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var expectedService = new Service( nameof(Should_return_service_from_k8s_on_first_call), new("localhost", 8080), string.Empty, string.Empty, Array.Empty()); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new[] { expectedService }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy receivedToken); // Act - First call should perform initial poll var services = await given.Provider.GetAsync(); // Assert services.ShouldNotBeNull(); services.Count.ShouldBe(1); services[0].HostAndPort.DownstreamHost.ShouldBe("localhost"); services[0].HostAndPort.DownstreamPort.ShouldBe(8080); receivedToken.Value.ShouldContain("Bearer"); } [Fact] [Trait("Feature", "Polling")] [Trait("Concurrency", "Multiple")] public async Task Should_return_queued_service_on_concurrent_calls() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var expectedService = new Service( nameof(Should_return_queued_service_on_concurrent_calls), new("localhost", 9090), string.Empty, string.Empty, Array.Empty()); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new[] { expectedService }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy receivedToken); // Act - First call to populate queue var firstCall = await given.Provider.GetAsync(); firstCall.ShouldNotBeNull(); firstCall.Count.ShouldBe(1); // Act - Multiple concurrent calls should return queued service var tasks = Enumerable.Range(0, 5) .Select(_ => given.Provider.GetAsync()) .ToArray(); var results = await Task.WhenAll(tasks); // Assert results.ShouldAllBe(r => r != null && r.Count == 1); results.ShouldAllBe(r => r[0].HostAndPort.DownstreamPort == 9090); } [Fact] [Trait("Feature", "Polling")] [Trait("Timing", "Interval")] public async Task Should_poll_at_specified_intervals() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder, pollingInterval: 50); var callCount = 0; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => { Interlocked.Increment(ref callCount); return new Service[] { new("service", new("localhost", callCount * 1000), string.Empty, string.Empty, Array.Empty()) }; }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act var firstServices = await given.Provider.GetAsync(); firstServices.ShouldNotBeNull(); // Wait for polling interval to elapse and check if new service version is queued await Task.Delay(PollingInterval, Xunit.TestContext.Current.CancellationToken); // Wait for at least one or two polling cycles var secondServices = await given.Provider.GetAsync(); // Assert - Services should not be empty and polling should have occurred secondServices.ShouldNotBeNull(); callCount.ShouldBeGreaterThanOrEqualTo(1); } [Fact] [Trait("Feature", "QueueManagement")] [Trait("Behavior", "OldVersionRemoval")] public async Task Should_remove_outdated_versions_and_keep_latest() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var version = 0; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => { version++; return new Service[] { new($"service-v{version}", new("localhost", version), string.Empty, string.Empty, Array.Empty()) }; }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act var firstCall = await given.Provider.GetAsync(); firstCall.ShouldNotBeNull(); // Wait for multiple polling cycles await Task.Delay(300, Xunit.TestContext.Current.CancellationToken); var lastCall = await given.Provider.GetAsync(); // Assert - Should get the latest version with the highest port number lastCall.ShouldNotBeNull(); lastCall.Count.ShouldBe(1); lastCall[0].HostAndPort.DownstreamPort.ShouldBeGreaterThan(1); } [Fact] [Trait("Feature", "ErrorHandling")] public async Task Should_return_empty_when_provider_disposed() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new Service[] { new("test", new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act var services = await given.Provider.GetAsync(); services.ShouldNotBeNull(); // Dispose the provider given.Provider.Dispose(); await Task.Delay(200, Xunit.TestContext.Current.CancellationToken); // Try to get services after disposal - should return empty var servicesAfterDisposal = await given.Provider.GetAsync(); // Assert servicesAfterDisposal.ShouldNotBeNull(); servicesAfterDisposal.Count.ShouldBe(0); } [Fact] [Trait("Feature", "ErrorHandling")] [Trait("Scenario", "KubeAPIError")] public async Task Should_handle_k8s_api_error_gracefully() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new Service[] { new("test", new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.InternalServerError, endpoints, out Lazy _); // Act var services = await given.Provider.GetAsync(); // Assert - Should return empty list on error services.ShouldNotBeNull(); // First call may return empty due to API error } [Fact(Skip = "Under development")] [Trait("Feature", "ColdStart")] public async Task Should_perform_initial_poll_on_first_call_when_queue_is_empty() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var initialService = new Service( "initial-service", new("localhost", 5000), string.Empty, string.Empty, Array.Empty()); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new[] { initialService }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act - First call on newly created provider with empty queue var services = await given.Provider.GetAsync(); // Assert services.ShouldNotBeNull(); services.Count.ShouldBe(1); services[0].Name.ShouldBe("initial-service"); services[0].HostAndPort.DownstreamPort.ShouldBe(5000); } [Fact] [Trait("Feature", "QueueManagement")] public async Task Should_not_enqueue_services_when_already_polling() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); var pollCount = 0; serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(() => { Interlocked.Increment(ref pollCount); return new Service[] { new($"service-poll-{pollCount}", new("localhost", 8000), string.Empty, string.Empty, Array.Empty()) }; }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act var firstCall = await given.Provider.GetAsync(); firstCall.ShouldNotBeNull(); // Assert pollCount.ShouldBeGreaterThanOrEqualTo(1); } [Fact(Skip = "Under development")] [Trait("Feature", "Threading")] public async Task Should_safely_handle_disposal_during_polling() { // Arrange var given = GivenClientAndPollKubeProvider(out var serviceBuilder); serviceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns(new Service[] { new("test", new("localhost", 80), string.Empty, string.Empty, Array.Empty()) }); var endpoints = GivenEndpoints(); GivenThereIsAFakeKubeServiceDiscoveryProvider( given.ClientOptions.ApiEndPoint.ToString(), given.ProviderOptions.KubeNamespace, given.ProviderOptions.KeyOfServiceInK8s, statusCode: HttpStatusCode.OK, endpoints, out Lazy _); // Act var getServiceTask = given.Provider.GetAsync(); await Task.Delay(10, Xunit.TestContext.Current.CancellationToken); // Let the polling start given.Provider.Dispose(); // Assert - Provider should be disposed var servicesAfterDisposal = await getServiceTask; servicesAfterDisposal.ShouldNotBeNull(); servicesAfterDisposal.Count.ShouldBe(0); } #region Helper Methods private (IKubeApiClient Client, KubeClientOptions ClientOptions, PollKube Provider, KubeRegistryConfiguration ProviderOptions) GivenClientAndPollKubeProvider(out Mock serviceBuilder, int pollingInterval = PollingInterval, [CallerMemberName] string serviceName = null) { serviceName ??= serviceName; var kubePort = PortFinder.GetRandomPort(); var options = new KubeClientOptions { AccessToken = serviceName, // "txpc696iUhbVoudg164r93CxDTrKRVWG", AllowInsecure = true, ApiEndPoint = new(DownstreamUrl(kubePort)), AuthStrategy = KubeAuthStrategy.BearerToken, }; IKubeApiClient client = KubeApiClient.Create(options); var config = new KubeRegistryConfiguration { KeyOfServiceInK8s = serviceName, KubeNamespace = nameof(PollKubeIntegrationTests), }; serviceBuilder = new(); // Create the inner Kube provider var kubeProvider = new Kube(config, _factory.Object, client, serviceBuilder.Object); // Wrap with PollKube var provider = new PollKube(pollingInterval, _factory.Object, kubeProvider); return (client, options, provider, config); } protected void GivenThereIsAFakeKubeServiceDiscoveryProvider( string url, string namespaces, string serviceName, EndpointsV1 endpointEntries, out Lazy receivedToken) => GivenThereIsAFakeKubeServiceDiscoveryProvider(url, namespaces, serviceName, HttpStatusCode.OK, endpointEntries, out receivedToken); protected void GivenThereIsAFakeKubeServiceDiscoveryProvider( string url, string namespaces, string serviceName, HttpStatusCode statusCode, EndpointsV1 endpointEntries, out Lazy receivedToken) { var token = string.Empty; receivedToken = new(() => token); handler.GivenThereIsAServiceRunningOn(url, async context => { if (context.Request.Path.Value != $"/api/v1/namespaces/{namespaces}/endpoints/{serviceName}") return; var responseBody = string.Empty; if (context.Request.Headers.TryGetValue("Authorization", out var values)) { token = values.First(); } responseBody = statusCode == HttpStatusCode.OK ? JsonConvert.SerializeObject(endpointEntries, JsonSerializerSettings) : JsonConvert.SerializeObject(new StatusV1 { Message = GetKubeApiErrorMessage(serviceName, namespaces, statusCode), Reason = statusCode.ToString(), Code = (int)statusCode, Status = StatusV1.FailureStatus, }, JsonSerializerSettings); context.Response.StatusCode = (int)statusCode; context.Response.Headers.Append("Content-Type", "application/json"); await context.Response.WriteAsync(responseBody); }); } private static EndpointsV1 GivenEndpoints() { var endpoints = new EndpointsV1 { Metadata = new ObjectMetaV1 { Name = "test-endpoints", Namespace = nameof(PollKubeIntegrationTests) }, }; var subset = new EndpointSubsetV1(); endpoints.Subsets.Add(subset); subset.Addresses.Add(new() { Ip = "127.0.0.1", TargetRef = new ObjectReferenceV1 { Name = "pod-1" }, }); subset.Ports.Add(new() { Name = "http", Port = 8080, }); return endpoints; } private static string GetKubeApiErrorMessage(string serviceName, string kubeNamespace, HttpStatusCode responseStatusCode) { return responseStatusCode switch { HttpStatusCode.NotFound => $"endpoints \"{serviceName}\" not found", HttpStatusCode.Forbidden => $"endpoints \"{serviceName}\" is forbidden: User \"system:serviceaccount:default:default\" cannot get resource \"endpoints\" in API group \"\" in the namespace \"{kubeNamespace}\"", HttpStatusCode.BadRequest => $"Bad Request: endpoints \"{serviceName}\" in namespace \"{kubeNamespace}\" is invalid", _ => $"Failed to retrieve endpoints \"{serviceName}\" in namespace \"{kubeNamespace}\"", }; } #endregion } ================================================ FILE: test/Ocelot.AcceptanceTests/ServiceDiscovery/ServiceFabricTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests.ServiceDiscovery; public sealed class ServiceFabricTests : Steps { private string _downstreamPath; public ServiceFabricTests() { } [Fact] [Trait("PR", "570")] [Trait("Bug", "555")] public void Should_fix_issue_555() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/{everything}", DownstreamScheme = "http", UpstreamPathTemplate = "/{everything}", UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Host = "localhost", Port = port, Type = "ServiceFabric", }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/OcelotServiceApplication/OcelotApplicationService/a", 200, "Hello from Laura", "b=c")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/a?b=c")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_support_service_fabric_naming_and_dns_service_stateless_and_guest() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", UpstreamPathTemplate = "/EquipmentInterfaces", UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Host = "localhost", Port = port, Type = "ServiceFabric", }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "test=best")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?test=best")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_support_service_fabric_naming_and_dns_service_statefull_and_actors() { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List { new() { DownstreamPathTemplate = "/api/values", DownstreamScheme = "http", UpstreamPathTemplate = "/EquipmentInterfaces", UpstreamHttpMethod = ["Get"], ServiceName = "OcelotServiceApplication/OcelotApplicationService", }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Host = "localhost", Port = port, Type = "ServiceFabric", }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/OcelotServiceApplication/OcelotApplicationService/api/values", 200, "Hello from Laura", "PartitionKind=test&PartitionKey=1")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/EquipmentInterfaces?PartitionKind=test&PartitionKey=1")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Theory] [Trait("PR", "722")] [Trait("Feat", "721")] [InlineData("/api/{version}/values", "/values", "Service_{version}/Api", "/Service_1.0/Api/values", "/api/1.0/values?test=best", "test=best")] [InlineData("/api/{version}/{all}", "/{all}", "Service_{version}/Api", "/Service_1.0/Api/products", "/api/1.0/products?test=the-best-from-Aly", "test=the-best-from-Aly")] public void should_support_placeholder_in_service_fabric_service_name(string upstream, string downstream, string serviceName, string downstreamUrl, string url, string query) { var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new() { new() { DownstreamPathTemplate = downstream, DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = upstream, UpstreamHttpMethod = [HttpMethods.Get], ServiceName = serviceName, }, }, GlobalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Host = "localhost", Port = port, Type = "ServiceFabric", }, }, }; this.Given(x => x.GivenThereIsAServiceRunningOn(port, downstreamUrl, 200, "Hello from Felix Boers", query)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway(url)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Felix Boers")) .BDDfy(); } private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode, string responseBody, string expectedQueryString) { handler.GivenThereIsAServiceRunningOn(port, basePath, async context => { _downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; if (_downstreamPath != basePath) { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync("downstream path didnt match base path"); } else { if (context.Request.QueryString.Value.Contains(expectedQueryString)) { context.Response.StatusCode = statusCode; await context.Response.WriteAsync(responseBody); } else { context.Response.StatusCode = (int)HttpStatusCode.NotFound; await context.Response.WriteAsync("downstream path didnt match base path"); } } }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/SslTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests; public sealed class SslTests : Steps { public SslTests() { } [Fact] public void Should_dangerous_accept_any_server_certificate_validator() { var port = PortFinder.GetRandomPort(); var route = GivenSslRoute(port, true); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } [Fact] public void Should_not_dangerous_accept_any_server_certificate_validator() { var port = PortFinder.GetRandomPort(); var route = GivenSslRoute(port, false); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.BadGateway)) .BDDfy(); } private FileRoute GivenSslRoute(int port, bool validatorEnabled) { var route = GivenDefaultRoute(port); route.DownstreamScheme = Uri.UriSchemeHttps; route.DangerousAcceptAnyServerCertificateValidator = validatorEnabled; return route; } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAHttpsServiceRunningOn(DownstreamUrl(port), basePath, "mycert2.pfx", "password", port, async context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool oK = downstreamPath == basePath; context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound; await context.Response.WriteAsync(oK ? responseBody : "downstream path didn't match base path"); }); } } ================================================ FILE: test/Ocelot.AcceptanceTests/StartupTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; using Ocelot.Responses; namespace Ocelot.AcceptanceTests; public class StartupTests : Steps { public StartupTests() { } [Fact] public void Should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); var fakeRepo = new FakeFileConfigurationRepository(); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpStatusCode.OK, "Hello from Laura")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => x.GivenOcelotIsRunningWithBlowingUpDiskRepo(fakeRepo)) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } private void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) { void WithFakeRepo(IServiceCollection s) => s.AddSingleton(fake).AddOcelot(); GivenOcelotIsRunning(WithFakeRepo); } private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode, string responseBody) { handler.GivenThereIsAServiceRunningOn(port, basePath, context => { var downstreamPath = !string.IsNullOrEmpty(context.Request.PathBase.Value) ? context.Request.PathBase.Value : context.Request.Path.Value; bool oK = downstreamPath == basePath; context.Response.StatusCode = oK ? (int)statusCode : (int)HttpStatusCode.NotFound; return context.Response.WriteAsync(oK ? responseBody : "downstream path didn't match base path"); }); } private class FakeFileConfigurationRepository : IFileConfigurationRepository { public Task> Get() => throw new NotImplementedException(); public Task Set(FileConfiguration fileConfiguration) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Steps.cs ================================================ using Ocelot.AcceptanceTests.Properties; using Ocelot.DependencyInjection; using Ocelot.Middleware; namespace Ocelot.AcceptanceTests; public class Steps : AcceptanceSteps { public Steps() : base() { BddfyConfig.Configure(); } public static bool IsCiCd() => IsRunningInGitHubActions(); public static bool IsRunningInGitHubActions() => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true"; public void GivenOcelotIsRunningWithDelegatingHandler(bool global = false) where THandler : DelegatingHandler => GivenOcelotIsRunning(s => s.AddOcelot().AddDelegatingHandler(global)); public Task GivenOcelotIsRunningAsync(OcelotPipelineConfiguration pipelineConfig) => GivenOcelotIsRunningAsync(WithBasicConfiguration, WithAddOcelot, async a => await a.UseOcelot(pipelineConfig)); #region TODO: Move to Ocelot.Testing package #endregion } ================================================ FILE: test/Ocelot.AcceptanceTests/Transformations/ClaimsToDownstreamPathTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Ocelot.AcceptanceTests.Authorization; using Ocelot.Testing.Authentication; namespace Ocelot.AcceptanceTests.Transformations; /// /// Feature: Claims to Downstream Path. /// [Trait("Feat", "968")] // https://github.com/ThreeMammals/Ocelot/pull/968 [Trait("Release", "13.8.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0 public sealed class ClaimsToDownstreamPathTests : AuthorizationSteps { [Fact] public void Should_return_200_OK_and_change_downstream_path() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["openid", "offline_access", "api"]; var route = GivenAuthRoute(port, scopes: allowedScopes); route.DownstreamPathTemplate = "/users/{userId}"; route.UpstreamPathTemplate = "/users/{userId}"; route.ChangeDownstreamPathTemplate = new() { { "userId", $"Claims[{OcelotClaims.OcSub}] > value[1] > |" }, }; var configuration = GivenConfiguration(route); var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Victor")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveAToken(testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/users")) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("Hello from Victor")) .And(x => ThenTheDownstreamPathIs("/users/1234567890")) .BDDfy(); } private const string UserId = "1234567890"; protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e) { e.Request.UserId += "|" + UserId; // -> sub claim -> oc-sub claim } private string _downstreamFinalPath; private void ThenTheDownstreamPathIs(string path) { _downstreamFinalPath.ShouldBe(path); } protected override Task MapStatus(HttpContext context) { _downstreamFinalPath = context.Request.Path.Value; return base.MapStatus(context); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Transformations/ClaimsToHeadersForwardingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.AcceptanceTests.Authorization; using Ocelot.Infrastructure.Extensions; using Ocelot.Testing.Authentication; namespace Ocelot.AcceptanceTests.Transformations; /// /// Feature: Claims to Headers. /// [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public sealed class ClaimsToHeadersForwardingTests : AuthorizationSteps { [Fact] public void Should_return_200_OK_and_forward_claim_as_header() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["openid", "offline_access", "api"]; var route = GivenAuthRoute(port, scopes: allowedScopes); route.AddHeadersToRequest = new() { { "CustomerId", "Claims[CustomerId] > value" }, { "LocationId", "Claims[LocationId] > value" }, { "UserType", $"Claims[{OcelotClaims.OcSub}] > value[0] > |" }, { "UserId", $"Claims[{OcelotClaims.OcSub}] > value[1] > |" }, }; var configuration = GivenConfiguration(route); var claims = new Dictionary() { { "CustomerId", "111" }, { "LocationId", "222" }, }; var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Tom")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("Hello from Tom")) .And(x => ThenTheResponseHeaderIs("RequestHeaders", "CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_header UserId:1234567890")) .BDDfy(); } private const string UserId = "1234567890"; protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e) { e.Request.UserId += "|" + UserId; // -> sub claim -> oc-sub claim } protected override Task MapStatus(HttpContext context) { var customerId = context.Request.Headers.GetCommaSeparatedValues("CustomerId").Csv(); var locationId = context.Request.Headers.GetCommaSeparatedValues("LocationId").Csv(); var userType = context.Request.Headers.GetCommaSeparatedValues("UserType").Csv(); var userId = context.Request.Headers.GetCommaSeparatedValues("UserId").Csv(); var responseBody = $"CustomerId:{customerId} LocationId:{locationId} UserType:{userType} UserId:{userId}"; context.Response.Headers.Append("RequestHeaders", responseBody); return base.MapStatus(context); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Transformations/ClaimsToQueryStringForwardingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.AcceptanceTests.Authorization; using Ocelot.Configuration.File; using Ocelot.Testing.Authentication; namespace Ocelot.AcceptanceTests.Transformations; /// /// Feature: Claims to Query String Parameters. /// [Trait("Commit", "f7f4a39")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93 [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public sealed class ClaimsToQueryStringForwardingTests : AuthorizationSteps { private static Dictionary GivenAddQueriesToRequest(FileRoute route) { route.AddQueriesToRequest = new() { { "CustomerId", "Claims[CustomerId] > value" }, { "LocationId", "Claims[LocationId] > value" }, { "UserType", $"Claims[{OcelotClaims.OcSub}] > value[0] > |" }, { "UserId", $"Claims[{OcelotClaims.OcSub}] > value[1] > |" }, }; var claims = new Dictionary() { { "CustomerId", "111" }, { "LocationId", "222" }, }; return claims; } [Fact] public void Should_return_200_OK_and_forward_claim_as_query_string() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["openid", "offline_access", "api"]; var route = GivenAuthRoute(port, scopes: allowedScopes); var configuration = GivenConfiguration(route); var claims = GivenAddQueriesToRequest(route); var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Tom")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_query_string UserId:1234567890")) .And(x => ThenTheQueryStringIs("?CustomerId=111&LocationId=222&UserId=1234567890&UserType=Should_return_200_OK_and_forward_claim_as_query_string")) .BDDfy(); } [Fact] public void Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string() { var port = PortFinder.GetRandomPort(); string[] allowedScopes = ["openid", "offline_access", "api"]; var route = GivenAuthRoute(port, scopes: allowedScopes); var configuration = GivenConfiguration(route); var claims = GivenAddQueriesToRequest(route); var testName = TestName(); this.Given(x => GivenThereIsExternalJwtSigningService(allowedScopes, Xunit.TestContext.Current.CancellationToken)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning(WithJwtBearerAuthentication)) .And(x => x.GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, "Hello from Tom")) .And(x => GivenIUpdateSubClaim()) .And(x => GivenIHaveATokenWithClaims(claims, testName)) .And(x => GivenIHaveAddedATokenToMyRequest()) .When(x => WhenIGetUrlOnTheApiGateway("/?test=1&test=2")) .Then(x => ThenTheStatusCodeShouldBeOK()) .And(x => ThenTheResponseBodyShouldBe("CustomerId:111 LocationId:222 UserType:Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string UserId:1234567890")) .And(x => ThenTheQueryStringIs("?test=1&test=2&CustomerId=111&LocationId=222&UserId=1234567890&UserType=Should_return_200_OK_and_forward_claim_as_query_string_and_preserve_original_string")) .BDDfy(); } private string _downstreamQueryString; private void ThenTheQueryStringIs(string queryString) { _downstreamQueryString.ShouldBe(queryString); } private const string UserId = "1234567890"; protected override void UpdateSubClaim(object sender, AuthenticationTokenRequestEventArgs e) { e.Request.UserId += "|" + UserId; // -> sub claim -> oc-sub claim } protected override Task MapStatus(HttpContext context) { _downstreamQueryString = context.Request.QueryString.Value; context.Request.Query.TryGetValue("CustomerId", out var customerId); context.Request.Query.TryGetValue("LocationId", out var locationId); context.Request.Query.TryGetValue("UserType", out var userType); context.Request.Query.TryGetValue("UserId", out var userId); MapStatus_ResponseBody = _ => $"CustomerId:{customerId} LocationId:{locationId} UserType:{userType} UserId:{userId}"; return base.MapStatus(context); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Transformations/HeaderTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Middleware; using System.Net.Sockets; using System.Text; namespace Ocelot.AcceptanceTests.Transformations; /// /// Ocelot feature: Headers Transformation. /// Read the Docs: Headers Transformation. /// [Trait("Feat", "204")] // https://github.com/ThreeMammals/Ocelot/pull/204 public sealed class HeaderTests : Steps { private static FileHttpHandlerOptions DoNotAllowAutoRedirect => new() { AllowAutoRedirect = false }; private static FileHttpHandlerOptions UseCookieContainer => new() { UseCookieContainer = true }; private static FileHttpHandlerOptions DoNotUseCookieContainer => new() { UseCookieContainer = false }; [Fact] public void Should_transform_upstream_header() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.UpstreamHeaderTransform.Add("Laz", "D, GP"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, "Laz")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader("Laz", "D")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Laz: GP")) .BDDfy(); } [Fact] public void Should_transform_downstream_header() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.DownstreamHeaderTransform.Add("Location", "http://www.bbc.co.uk/, http://ocelot.net/"); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.OK, "Location", "http://www.bbc.co.uk/")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseHeaderIs("Location", "http://ocelot.net/")) .BDDfy(); } [Fact] [Trait("Feat", "190")] // https://github.com/ThreeMammals/Ocelot/issues/190 public void Should_fix_issue_190() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.DownstreamHeaderTransform.Add("Location", $"{DownstreamUrl(port)}, {{BaseUrl}}"); route.HttpHandlerOptions = DoNotAllowAutoRedirect; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, "Location", $"{DownstreamUrl(port)}/pay/Receive")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) #if NET10_0_OR_GREATER .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect("/", DoNotAllowAutoRedirect.AllowAutoRedirect.Value)) #else .When(x => WhenIGetUrlOnTheApiGateway("/")) #endif .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) .And(x => ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) .BDDfy(); } [Fact] [Trait("Feat", "205")] // https://github.com/ThreeMammals/Ocelot/issues/205 public void Should_fix_issue_205() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.DownstreamHeaderTransform.Add("Location", "{DownstreamBaseUrl}, {BaseUrl}"); route.HttpHandlerOptions = DoNotAllowAutoRedirect; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, "Location", $"{DownstreamUrl(port)}/pay/Receive")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) #if NET10_0_OR_GREATER .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect("/", DoNotAllowAutoRedirect.AllowAutoRedirect.Value)) #else .When(x => WhenIGetUrlOnTheApiGateway("/")) #endif .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) .And(x => ThenTheResponseHeaderIs("Location", "http://localhost:5000/pay/Receive")) .BDDfy(); } [Fact] [Trait("Feat", "417")] // https://github.com/ThreeMammals/Ocelot/issues/417 public void Should_fix_issue_417() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.DownstreamHeaderTransform.Add("Location", "{DownstreamBaseUrl}, {BaseUrl}"); route.HttpHandlerOptions = DoNotAllowAutoRedirect; var configuration = GivenConfiguration(route); configuration.GlobalConfiguration.BaseUrl = "http://anotherapp.azurewebsites.net"; this.Given(x => x.GivenThereIsAServiceReturningAHeaderBack(port, HttpStatusCode.Found, "Location", $"{DownstreamUrl(port)}/pay/Receive")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) #if NET10_0_OR_GREATER .When(x => WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect("/", DoNotAllowAutoRedirect.AllowAutoRedirect.Value)) #else .When(x => WhenIGetUrlOnTheApiGateway("/")) #endif .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.Redirect)) .And(x => ThenTheResponseHeaderIs("Location", "http://anotherapp.azurewebsites.net/pay/Receive")) .BDDfy(); } [Fact] [Trait("Bug", "274")] // https://github.com/ThreeMammals/Ocelot/issues/274 public void Request_should_reuse_cookies_with_cookie_container() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/sso/{everything}", "/sso/{everything}"); route.UpstreamHttpMethod = [HttpMethods.Get, HttpMethods.Post, HttpMethods.Options]; route.HttpHandlerOptions = UseCookieContainer; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/sso/test", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => WhenIGetUrlOnTheApiGateway("/sso/test")) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) .And(x => GivenIAddCookieToMyRequest("test=1; path=/")) .When(x => WhenIGetUrlOnTheApiGateway("/sso/test")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] [Trait("Bug", "274")] // https://github.com/ThreeMammals/Ocelot/issues/274 public void Request_should_have_own_cookies_no_cookie_container() { var port = PortFinder.GetRandomPort(); var route = GivenRoute(port, "/sso/{everything}", "/sso/{everything}"); route.UpstreamHttpMethod = [HttpMethods.Get, HttpMethods.Post, HttpMethods.Options]; route.HttpHandlerOptions = DoNotUseCookieContainer; var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/sso/test", HttpStatusCode.OK)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => WhenIGetUrlOnTheApiGateway("/sso/test")) .And(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseHeaderIs("Set-Cookie", "test=0; path=/")) .And(x => GivenIAddCookieToMyRequest("test=1; path=/")) .When(x => WhenIGetUrlOnTheApiGateway("/sso/test")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] [Trait("Bug", "474")] // https://github.com/ThreeMammals/Ocelot/issues/474 [Trait("PR", "483")] // https://github.com/ThreeMammals/Ocelot/pull/483 public void Issue_474_should_not_put_spaces_in_header() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, "Accept")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader("Accept", "text/html,application/xhtml+xml,application/xml;")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Accept: text/html,application/xhtml+xml,application/xml;")) .BDDfy(); } [Fact] [Trait("Bug", "474")] [Trait("PR", "483")] public void Issue_474_should_put_spaces_in_header() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, "Accept")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .And(x => GivenIAddAHeader("Accept", "text/html")) .And(x => GivenIAddAHeader("Accept", "application/xhtml+xml")) .And(x => GivenIAddAHeader("Accept", "application/xml")) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => ThenTheResponseBodyShouldBe("Accept: text/html, application/xhtml+xml, application/xml")) .BDDfy(); } [Fact(DisplayName = "TODO Redevelop Placeholders as part of Header Transformation feat")] [Trait("Feat", "623")] // https://github.com/ThreeMammals/Ocelot/issues/623 [Trait("PR", "632")] // https://github.com/ThreeMammals/Ocelot/pull/632 public async Task Should_pass_remote_ip_address_if_as_x_forwarded_for_header() { var port = PortFinder.GetRandomPort(); var route = GivenDefaultRoute(port); route.UpstreamHeaderTransform.Add(X_Forwarded_For, "{RemoteIpAddress}"); route.HttpHandlerOptions = DoNotAllowAutoRedirect; var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); GivenThereIsAServiceEchoingAHeader(port, HttpStatusCode.OK, X_Forwarded_For); var ocelotIP = string.Empty; var pipeline = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { ocelotIP = GetRemoteIpAddress(ctx); await next.Invoke(); }, }; await GivenOcelotIsRunningAsync(pipeline); //var remoteIpAddress = Dns.GetHostAddresses("dns.google").First(a => a.AddressFamily != System.Net.Sockets.AddressFamily.InterNetworkV6).ToString(); //GivenIAddAHeader(X_Forwarded_For, remoteIpAddress); await WhenIGetUrlOnTheApiGateway("/"); ThenTheStatusCodeShouldBe(HttpStatusCode.OK); await ThenTheResponseBodyShouldBeAsync("X-Forwarded-For: " + /*remoteIpAddress*/ocelotIP); } public const string X_Forwarded_For = "X-Forwarded-For"; public const string X_Forwarded_Host = "X-Forwarded-Host"; public const string X_Forwarded_Proto = "X-Forwarded-Proto"; [Fact] [Trait("Feat", "1658")] // https://github.com/ThreeMammals/Ocelot/issues/1658 [Trait("PR", "1659")] // https://github.com/ThreeMammals/Ocelot/pull/1659 public async Task ShouldApplyGlobalUpstreamHeaderTransformsForAllRoutes() { const string Ot_Route = "Ot-Route"; var port1 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/route1"); route1.UpstreamHeaderTransform.Add(Ot_Route, "Raman"); var port2 = PortFinder.GetRandomPort(); var route2 = GivenRoute(port2, "/route2"); route2.UpstreamHeaderTransform.Add(Ot_Route, "Mark"); var port3 = PortFinder.GetRandomPort(); var route3 = GivenRoute(port3, "/route3"); var configuration = GivenConfiguration(route1, route2, route3); configuration.GlobalConfiguration.BaseUrl = "http://ocelot.net"; configuration.GlobalConfiguration.UpstreamHeaderTransform = new Dictionary() { { X_Forwarded_For, "{RemoteIpAddress}" }, { X_Forwarded_Host, "{BaseUrl}" }, { X_Forwarded_Proto, "https" }, { Ot_Route, "?" }, }; var allHeaders = configuration.GlobalConfiguration.UpstreamHeaderTransform.Keys .Union(route1.UpstreamHeaderTransform.Keys.Intersect(route2.UpstreamHeaderTransform.Keys)) .ToArray(); GivenThereIsAServiceEchoingAHeader(port1, HttpStatusCode.OK, allHeaders); GivenThereIsAServiceEchoingAHeader(port2, HttpStatusCode.OK, allHeaders); GivenThereIsAServiceEchoingAHeader(port3, HttpStatusCode.OK, allHeaders); GivenThereIsAConfiguration(configuration); var ocelotIP = string.Empty; var pipeline = new OcelotPipelineConfiguration { PreErrorResponderMiddleware = async (ctx, next) => { ocelotIP = GetRemoteIpAddress(ctx); await next.Invoke(); }, }; await GivenOcelotIsRunningAsync(pipeline); await WhenIGetUrlOnTheApiGateway("/route1"); ThenTheResponseBodyShouldBe(@$"X-Forwarded-For: {ocelotIP} X-Forwarded-Host: http://ocelot.net X-Forwarded-Proto: https Ot-Route: Raman"); await WhenIGetUrlOnTheApiGateway("/route2"); ThenTheResponseBodyShouldBe(@$"X-Forwarded-For: {ocelotIP} X-Forwarded-Host: http://ocelot.net X-Forwarded-Proto: https Ot-Route: Mark"); await WhenIGetUrlOnTheApiGateway("/route3"); ThenTheResponseBodyShouldBe(@$"X-Forwarded-For: {ocelotIP} X-Forwarded-Host: http://ocelot.net X-Forwarded-Proto: https Ot-Route: ?"); } [Fact] [Trait("Feat", "1658")] // https://github.com/ThreeMammals/Ocelot/issues/1658 [Trait("PR", "1659")] // https://github.com/ThreeMammals/Ocelot/pull/1659 public async Task ShouldApplyGlobalDownstreamHeaderTransformsForAllRoutes() { const string Who = "Who", X_Forwarded_By = "X-Forwarded-By"; var port1 = PortFinder.GetRandomPort(); var route1 = GivenRoute(port1, "/route1"); route1.DownstreamHeaderTransform.Add(Who, "Raman, Mark"); var port2 = PortFinder.GetRandomPort(); var route2 = GivenRoute(port2, "/route2"); route2.DownstreamHeaderTransform.Add(Who, "Mark, Raman"); var port3 = PortFinder.GetRandomPort(); var route3 = GivenRoute(port3, "/route3"); var configuration = GivenConfiguration(route1 ,route2, route3); configuration.GlobalConfiguration.BaseUrl = "http://ocelot.net"; configuration.GlobalConfiguration.DownstreamHeaderTransform = new Dictionary() { { X_Forwarded_By, "{BaseUrl}" }, { Who, "HideMe, ?" }, }; GivenThereIsAServiceReturningAHeaderBack(port1, HttpStatusCode.OK, Who, "Raman Mark Raman"); GivenThereIsAServiceReturningAHeaderBack(port2, HttpStatusCode.OK, Who, "Mark Raman Mark"); GivenThereIsAServiceReturningAHeaderBack(port3, HttpStatusCode.OK, Who, "HideMe Mark"); GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(); await WhenIGetUrlOnTheApiGateway("/route1"); ThenTheResponseHeaderExists(Who); ThenTheResponseHeaderIs(Who, "Mark Mark Mark"); ThenTheResponseHeaderExists(X_Forwarded_By); ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl); await WhenIGetUrlOnTheApiGateway("/route2"); ThenTheResponseHeaderIs(Who, "Raman Raman Raman"); ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl); await WhenIGetUrlOnTheApiGateway("/route3"); ThenTheResponseHeaderIs(Who, "? Mark"); ThenTheResponseHeaderIs(X_Forwarded_By, configuration.GlobalConfiguration.BaseUrl); } private static string GetRemoteIpAddress(HttpContext context) { var ip = context.Connection.RemoteIpAddress ?? Dns.GetHostAddresses(string.Empty).FirstOrDefault(a => a.AddressFamily != AddressFamily.InterNetworkV6); return ip.ToString(); } private int _count; private void GivenThereIsAServiceRunningOn(int port, string basePath, HttpStatusCode statusCode) { Task MapCookies(HttpContext context) { if (_count == 0) { context.Response.Cookies.Append("test", "0"); _count++; context.Response.StatusCode = (int)statusCode; return Task.CompletedTask; } else if (context.Request.Cookies.TryGetValue("test", out var cookieValue)) { if (cookieValue == "0") { context.Response.StatusCode = (int)statusCode; return Task.CompletedTask; } } else if (context.Request.Headers.TryGetValue("Set-Cookie", out var headerValue)) { if (headerValue == "test=1; path=/") { context.Response.StatusCode = (int)statusCode; return Task.CompletedTask; } } context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; return Task.CompletedTask; } handler.GivenThereIsAServiceRunningOn(port, basePath, MapCookies); } private void GivenThereIsAServiceReturningAHeaderBack(int port, HttpStatusCode statusCode, string headerKey, string headerValue) { Task MapHeaderIntoResponseHeaders(HttpContext context) { context.Response.OnStarting(() => { context.Response.Headers.Append(headerKey, headerValue); context.Response.StatusCode = (int)statusCode; return Task.CompletedTask; }); return Task.CompletedTask; } handler.GivenThereIsAServiceRunningOn(port, MapHeaderIntoResponseHeaders); } private void GivenThereIsAServiceEchoingAHeader(int port, HttpStatusCode statusCode, params string[] headerKeys) { Task MapHeaderIntoResponseBody(HttpContext context) { var body = new StringBuilder(); foreach (var key in headerKeys) { if (context.Request.Headers.TryGetValue(key, out var values)) { body.AppendLine($"{key}: {values}"); } } context.Response.StatusCode = (int)statusCode; body.Length -= Environment.NewLine.Length; return context.Response.WriteAsync(body.ToString()); } handler.GivenThereIsAServiceRunningOn(port, MapHeaderIntoResponseBody); } /// /// Using HttpClientHandler to configure auto-redirect behavior /// private async Task WhenIGetUrlOnTheApiGatewayWithAllowAutoRedirect(string url, bool allowAutoRedirect) { var handler = new HttpClientHandler() { AllowAutoRedirect = allowAutoRedirect, }; var baseAddr = ocelotClient.BaseAddress; ocelotClient = new(handler) { BaseAddress = baseAddr, }; response = await ocelotClient.GetAsync(url); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Transformations/MethodTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; namespace Ocelot.AcceptanceTests.Transformations; public sealed class MethodTests : Steps { public MethodTests() { } [Fact] public void Should_return_response_200_when_get_converted_to_post() { var port = PortFinder.GetRandomPort(); var route = GivenRouteWithMethods(port); var configuration = GivenConfiguration(route); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpMethods.Post)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } [Fact] public void Should_return_response_200_when_get_converted_to_post_with_content() { var port = PortFinder.GetRandomPort(); var route = GivenRouteWithMethods(port); var configuration = GivenConfiguration(route); const string expected = "here is some content"; var httpContent = new StringContent(expected); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpMethods.Post)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/", httpContent)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } [Fact] public void Should_return_response_200_when_get_converted_to_get_with_content() { var port = PortFinder.GetRandomPort(); var route = GivenRouteWithMethods(port, HttpMethods.Post, HttpMethods.Get); var configuration = GivenConfiguration(route); const string expected = "here is some content"; var httpContent = new StringContent(expected); this.Given(x => x.GivenThereIsAServiceRunningOn(port, "/", HttpMethods.Get)) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIPostUrlOnTheApiGateway("/", httpContent)) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(_ => ThenTheResponseBodyShouldBe(expected)) .BDDfy(); } private FileRoute GivenRouteWithMethods(int port, string up = null, string down = null) => new() { DownstreamPathTemplate = "/{url}", DownstreamScheme = Uri.UriSchemeHttp, UpstreamPathTemplate = "/{url}", UpstreamHttpMethod = [up ?? HttpMethods.Get], DownstreamHostAndPorts = [ Localhost(port) ], DownstreamHttpMethod = down ?? HttpMethods.Post, }; private void GivenThereIsAServiceRunningOn(int port, string basePath, string expected) { async Task MapMethod(HttpContext context) { if (context.Request.Method == expected) { context.Response.StatusCode = (int)HttpStatusCode.OK; var reader = new StreamReader(context.Request.Body); var body = await reader.ReadToEndAsync(); await context.Response.WriteAsync(body); } else { context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; } } handler.GivenThereIsAServiceRunningOn(port, basePath, MapMethod); } } ================================================ FILE: test/Ocelot.AcceptanceTests/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using Moq; global using Ocelot; global using Ocelot.Testing; global using Ocelot.Testing.Boxing; global using Shouldly; global using System.Net; global using TestStack.BDDfy; global using Xunit; using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Reviewed.")] internal class Usings { } ================================================ FILE: test/Ocelot.AcceptanceTests/WebSockets/ClientWebSocketTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.AcceptanceTests.Logging; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Logging; using Ocelot.WebSockets; using System.Net.WebSockets; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; namespace Ocelot.AcceptanceTests.WebSockets; public sealed class ClientWebSocketTests : WebSocketsSteps { private readonly ClientWebSocket _ws = new(); private readonly CancellationTokenSource _cts = new(); public ClientWebSocketTests() { _cts.CancelAfter(3_500); // run (wait) all tests no more than 3.5 seconds } public override void Dispose() { _ws.Dispose(); _cts.Dispose(); base.Dispose(); } /// It tests the following stack: HTTP 1.1, SSL, WebSocket. /// A object. [Theory] [InlineData("ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx", null)] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets /* [InlineData("wss://echo.websocket.org", "Request served by ")] // https://websocket.org/tools/websocket-echo-server/ */ [InlineData("wss://ws.postman-echo.com/raw", null)] // https://blog.postman.com/introducing-postman-websocket-echo-service/ public async Task Http11CLient_DirectConnection_ShouldConnect(string url, string expected) { GivenOptions(); var echoEndpoint = new Uri(url); await _ws.ConnectAsync(echoEndpoint, _cts.Token); var actual = await WhenISendAndReceiveEchoMessage(); if (string.IsNullOrEmpty(expected)) Assert.Equal(Expected(), actual); else Assert.StartsWith(expected, actual); } /// It tests the following stack: HTTP/2, SSL, WebSocket. /// HTTP/2 always requires an SSL certificate. /// A object. [Fact] public async Task Http20CLient_DirectConnection_ShouldConnect() { int port = PortFinder.GetRandomPort(); await GivenWebSocketsHttp2ServiceIsRunningAsync(port, EchoAsync, _cts.Token); GivenHttp2Options(); var echoEndpoint = new UriBuilder(Uri.UriSchemeWss, /*"localhost"*/ "threemammals.com", port).Uri; using var handler = new HttpClientHandler { // ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true, PreAuthenticate = false, Credentials = new NetworkCredential("tom@threemammals.com", "password"), }; if (IsCiCd() && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) // MacOS handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true; // TODO Makes sense to log in console using var invoker = new HttpMessageInvoker(handler); await _ws.ConnectAsync(echoEndpoint, invoker, _cts.Token); var actual = await WhenISendAndReceiveEchoMessage(); Assert.Equal(Expected(), actual); } ///// In the middle, Ocelot tests the following stack: HTTP 1.1, SSL, WebSocket. ///// A object. [Theory] [InlineData("ws", "corefx-net-http11.azurewebsites.net", 80, "/WebSocket/EchoWebSocket.ashx", null)] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets /*[InlineData("wss","echo.websocket.org", 443, "/", "Request served by ")] // https://websocket.org/tools/websocket-echo-server/ */ [InlineData("wss", "ws.postman-echo.com", 443, "/raw", null)] // https://blog.postman.com/introducing-postman-websocket-echo-service/ public async Task Http11CLient_OcelotInTheMiddle_ShouldConnect(string scheme, string host, int port, string path, string expected) { var route = GivenWsRoute(scheme, host, port, path); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); int ocelotPort = PortFinder.GetRandomPort(); await StartOcelotWithWebSockets(ocelotPort, WithAddOcelot); GivenOptions(); var ocelot = new UriBuilder(Uri.UriSchemeWs, "localhost", ocelotPort).Uri; await _ws.ConnectAsync(ocelot, _cts.Token); var actual = await WhenISendAndReceiveEchoMessage(); if (string.IsNullOrEmpty(expected)) Assert.Equal(Expected(), actual); else Assert.StartsWith(expected, actual); } /// In the middle, Ocelot tests the following stack: HTTP/2, SSL, WebSocket. /// HTTP/2 always requires an SSL certificate.
/// TODO: Scenario of HTTP/2 (SSL) vs WebSocket is not supported by Ocelot's : see the ConnectAsync method. ///
/// A object. // Scenario of HTTP/2 (SSL) vs WebSocket is not supported by Ocelot's WebSocketsProxyMiddleware. // AI Q.1: websocket http/2 | What browsers and tools support this couple? // AI A.1: Proxy Servers: Implementing WebSocket support for HTTP/2 proxies requires handling the CONNECT request with a ':protocol' pseudo-header. // See Stack Overflow | Implementing websocket support for HTTP/2 proxies -> https://stackoverflow.com/questions/65129151/implementing-websocket-support-for-http-2-proxies // AI Q.2: C# Yarp | Does Yarp support webSocket+HTTP/2 forwarding? // AI A.2: Yes! YARP (Yet Another Reverse Proxy) supports WebSockets over HTTP/2 starting in .NET 7 and YARP 2.0. // See MS Learn | YARP Proxying WebSockets and SPDY -> https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/websockets [Fact(DisplayName = "TODO " + nameof(Http20CLient_OcelotInTheMiddle_ShouldConnect), Skip = "TODO: HTTP/2 (SSL) vs WebSocket is unsupported scenario by Ocelot Core currently, unfortunately...")] public async Task Http20CLient_OcelotInTheMiddle_ShouldConnect() { var port = PortFinder.GetRandomPort(); await GivenWebSocketsHttp2ServiceIsRunningAsync(port, EchoAsync, _cts.Token); var route = GivenWsRoute(Uri.UriSchemeWss, /*"localhost"*/ "threemammals.com", port); route.DownstreamHttpVersion = HttpVersion.Version20.ToString(); // 2.0 !!! route.DownstreamHttpVersionPolicy = nameof(HttpVersionPolicy.RequestVersionOrHigher); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); int ocelotPort = PortFinder.GetRandomPort(); await StartHttp2OcelotWithWebSockets(ocelotPort); GivenHttp2Options(); var ocelot = new UriBuilder(Uri.UriSchemeWss, "localhost", ocelotPort).Uri; using var handler = new HttpClientHandler { ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true, }; using var invoker = new HttpMessageInvoker(handler); await _ws.ConnectAsync(ocelot, invoker, _cts.Token); // TODO System.Net.WebSockets.WebSocketException : The server returned status code '500' when status code '200' was expected. var actual = await WhenISendAndReceiveEchoMessage(); Assert.Equal(Expected(), actual); } [Theory] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 [InlineData("ws", "corefx-net-http11.azurewebsites.net", 80, "/WebSocket/EchoWebSocket.ashx")] // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/websockets#differences-in-http11-and-http2-websockets /*[InlineData("wss", "echo.websocket.org", 443, "/")] // https://websocket.org/tools/websocket-echo-server/ */ [InlineData("wss", "ws.postman-echo.com", 443, "/raw")] // https://blog.postman.com/introducing-postman-websocket-echo-service/ public async Task Http11Client_ConnectionClosedPrematurely_ShouldCloseSocketsWithoutExceptions(string scheme, string host, int port, string path) { static void WithExtraLogging(IServiceCollection services) => services.AddOcelot() .Services.RemoveAll() .AddSingleton>(); var route = GivenWsRoute(scheme, host, port, path); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); int ocelotPort = PortFinder.GetRandomPort(); await StartOcelotWithWebSockets(ocelotPort, WithExtraLogging); GivenOptions(); var ocelot = new UriBuilder(Uri.UriSchemeWs, "localhost", ocelotPort).Uri; await _ws.ConnectAsync(ocelot, _cts.Token); //var ex = await WhenISendAndReceiveEchoMessage(); var upload = Encoding.UTF8.GetBytes(Expected()); await _ws.SendAsync(upload, WebSocketMessageType.Text, true, _cts.Token); var echo = new byte[1024]; var result = await _ws.ReceiveAsync(echo, _cts.Token); // Act var exc = await Assert.ThrowsAsync(() => { _cts.Cancel(); return _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, Expected() + " has been sent", _cts.Token); }); _ws.Abort(); // !!! after cancellation of operations, let the connection be disposed aka finalized await Task.Delay(1_000, Xunit.TestContext.Current.CancellationToken); var factory = ocelotHost.Services.GetService(); var logger = (factory as TestLoggerFactory).Logger; Assert.NotNull(logger); logger.Messages.ShouldNotBeEmpty(); // STEPS TO REPRODUCE with old code, based on commit: https://github.com/ThreeMammals/Ocelot/commit/0b794b39e26d8bb538006eb5834b841c893c6611 // Bug930_StepsToReproduce(logger); logger.Exceptions.ShouldBeEmpty(); // no errors on Ocelot's side, as they were swallowed in favor of logging a warning logger.Messages.ShouldNotContain(m => m.Contains(Bug930RootCause)); // no bug logger.Logbook.Contains(Bug930RootCause).ShouldBeFalse(); // no bug in the log try { logger.Messages.ShouldContain(m => m.Contains(Bugfix930ExpectedMessage)); // logged warning logger.Logbook.Contains(Bugfix930ExpectedMessage).ShouldBeTrue(); // logged warning } catch { // Be tolerant of attempted assertions, as they sometimes fail when the 'ConnectionClosedPrematurely' exception is not generated, thus the logbook is empty } } public const string Bug930RootCause = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseReceived'"; public const string Bugfix930ExpectedMessage = "WebSocketException when WebSocketErrorCode is ConnectionClosedPrematurely"; private static void Bug930_StepsToReproduce(MemoryLogger logger) { logger.Exceptions.ShouldNotBeEmpty(); string PrintExceptions() => string.Join(Environment.NewLine, logger.Exceptions.Select(e => $"{e.GetType().Name}: {e.Message}")); logger.Exceptions.ShouldContain(e => e.GetType() == typeof(WebSocketException), PrintExceptions()); logger.Exceptions.Count(e => e.GetType() == typeof(WebSocketException)).ShouldBe(1, PrintExceptions()); logger.Exceptions.Count.ShouldBe(1, PrintExceptions()); var ex = logger.Exceptions.First(); ex.ShouldBeOfType(); ex.Message.ShouldBe(Bug930RootCause); logger.Messages.ShouldContain(m => m.Contains(Bug930RootCause)); logger.Logbook.Contains(Bug930RootCause).ShouldBeTrue(); } private static string Expected([CallerMemberName] string testName = null) => testName ?? nameof(ClientWebSocketTests); private static FileRoute GivenWsRoute(string scheme, string host, int port, string downstreamPath = null) => new() { UpstreamPathTemplate = "/", DownstreamPathTemplate = downstreamPath ?? "/", DownstreamScheme = scheme ?? Uri.UriSchemeWs, DownstreamHostAndPorts = [ new(host, port) ], }; private void GivenOptions() { #if NET9_0_OR_GREATER // Keep-Alive strategy is PING/PONG // KeepAliveInterval is a positive finite TimeSpan, -AND- // KeepAliveTimeout is a positive finite TimeSpan _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1); _ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1); #else // Keep-Alive strategy is Unsolicited PONG // KeepAliveInterval is a positive finite TimeSpan, -AND- // KeepAliveTimeout is TimeSpan.Zero or Timeout.InfiniteTimeSpan _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1); #endif } private void GivenHttp2Options() { #if NET9_0_OR_GREATER // Keep-Alive strategy is PING/PONG // KeepAliveInterval is a positive finite TimeSpan, -AND- // KeepAliveTimeout is a positive finite TimeSpan _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1); _ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(1); #else // Keep-Alive strategy is Unsolicited PONG // KeepAliveInterval is a positive finite TimeSpan, -AND- // KeepAliveTimeout is TimeSpan.Zero or Timeout.InfiniteTimeSpan _ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(1); #endif _ws.Options.HttpVersion = HttpVersion.Version20; // !!! _ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; } private async Task WhenISendAndReceiveEchoMessage([CallerMemberName] string message = null) { var upload = Encoding.UTF8.GetBytes(message); await _ws.SendAsync(upload, WebSocketMessageType.Text, true, _cts.Token); var echo = new byte[1024]; var result = await _ws.ReceiveAsync(echo, _cts.Token); string actual = Encoding.UTF8.GetString(echo, 0, result.Count); await _ws.CloseAsync(WebSocketCloseStatus.NormalClosure, message + " has been sent", _cts.Token); return actual; } } ================================================ FILE: test/Ocelot.AcceptanceTests/WebSockets/WebSocketsFactoryTests.cs ================================================ using Ocelot.Configuration.File; using Ocelot.LoadBalancer.Balancers; namespace Ocelot.AcceptanceTests.WebSockets; public sealed class WebSocketsFactoryTests : WebSocketsSteps { [Fact] [Trait("Feat", "212")] [Trait("PR", "273")] // https://github.com/ThreeMammals/Ocelot/pull/273 public async Task ShouldProxyWebsocketInputToDownstreamService() { var port = PortFinder.GetRandomPort(); var route = GivenRoute("/ws", port); var configuration = GivenConfiguration(route); GivenThereIsAConfiguration(configuration); int ocelotPort = PortFinder.GetRandomPort(); var ocelotUrl = new UriBuilder(Uri.UriSchemeWs, "localhost", ocelotPort).Uri; await StartOcelotWithWebSockets(ocelotPort, null); await GivenWebSocketsServiceIsRunningAsync(port, "/ws", EchoAsync, CancellationToken.None); await StartClient(ocelotUrl); ThenTheReceivedCountIs(10); void ThenTheReceivedCountIs(int count) => _firstRecieved.Count.ShouldBe(count); } [Fact] [Trait("Feat", "212")] [Trait("PR", "273")] // https://github.com/ThreeMammals/Ocelot/pull/273 public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer() { int port1 = PortFinder.GetRandomPort(); int port2 = PortFinder.GetRandomPort(); var route = GivenRoute("/ws", port1, port2); route.LoadBalancerOptions = new(nameof(RoundRobin)); var configuration = GivenConfiguration(route); int ocelotPort = PortFinder.GetRandomPort(); this.Given(_ => GivenThereIsAConfiguration(configuration)) .And(_ => StartOcelotWithWebSockets(ocelotPort, null)) .And(_ => GivenWebSocketsServiceIsRunningAsync(port1, "/ws", EchoAsync, CancellationToken.None)) .And(_ => GivenWebSocketsServiceIsRunningAsync(port2, "/ws", MessageAsync, CancellationToken.None)) .When(_ => WhenIStartTheClients(ocelotPort)) .Then(_ => ThenBothDownstreamServicesAreCalled()) .BDDfy(); } private FileRoute GivenRoute(string downstream = null, params int[] ports) => new() { UpstreamPathTemplate = "/", DownstreamPathTemplate = downstream ?? "/ws", DownstreamScheme = Uri.UriSchemeWs, DownstreamHostAndPorts = ports.Select(Localhost).ToList(), }; } ================================================ FILE: test/Ocelot.AcceptanceTests/WebSockets/WebSocketsSteps.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Ocelot.Middleware; using Ocelot.WebSockets; using System.Net.WebSockets; using System.Text; namespace Ocelot.AcceptanceTests.WebSockets; public class WebSocketsSteps : Steps { private readonly WebSocketsFactory _factory = new(); private readonly List _secondRecieved = new(); protected readonly List _firstRecieved = new(); protected static void WithConsole(WebHostBuilderContext context, ILoggingBuilder logging) => logging .AddConfiguration(context.Configuration.GetSection("Logging")) .AddConsole(); protected static void WithConfiguration(WebHostBuilderContext hosting, IConfigurationBuilder config) => config .SetBasePath(hosting.HostingEnvironment.ContentRootPath); protected static void WithoutServices(IServiceCollection services) { } protected Task GivenWebSocketServiceIsRunningOnAsync(string url, Action options, Func, Task> middleware) => handler.GivenThereIsAServiceRunningOnAsync(url, WithConfiguration, WithConsole, WithoutServices, app => app.UseWebSockets().Use(middleware), web => web.UseUrls(url).ConfigureKestrel(options).UseKestrel() // UseKestrelHttpsConfiguration() ); protected void ThenBothDownstreamServicesAreCalled() { _firstRecieved.Count.ShouldBe(10); _firstRecieved.ForEach(x => x.ShouldBe("test")); _secondRecieved.Count.ShouldBe(10); _secondRecieved.ForEach(x => x.ShouldBe("chocolate")); } protected Task GivenWebSocketsServiceIsRunningAsync(int port, string path, Func webSocketHandler, CancellationToken token) { async Task TheMiddleware(HttpContext context, Func next) { if (context.Request.Path != path) { await next(); return; } if (context.WebSockets.IsWebSocketRequest) { var webSocket = await context.WebSockets.AcceptWebSocketAsync(); await webSocketHandler(webSocket, token); } else { context.Response.StatusCode = (int)HttpStatusCode.BadRequest; } } void NoOptions(KestrelServerOptions options) { } var url = DownstreamUrl(port); return GivenWebSocketServiceIsRunningOnAsync(url, NoOptions, TheMiddleware); } protected Task GivenWebSocketsHttp2ServiceIsRunningAsync(int port, Func webSocketHandler, CancellationToken token) { async Task TheMiddleware(HttpContext context, Func next) { if (context.WebSockets.IsWebSocketRequest) { var webSocket = await context.WebSockets.AcceptWebSocketAsync(); await webSocketHandler(webSocket, token); } else { await next(); context.Response.StatusCode = (int)HttpStatusCode.BadRequest; } } void WithOptions(KestrelServerOptions options) => options.ListenAnyIP(port, WithHttp2); var url = DownstreamUrl(port, Uri.UriSchemeHttps); return GivenWebSocketServiceIsRunningOnAsync(url, WithOptions, TheMiddleware); } protected static async Task EchoAsync(WebSocket ws, CancellationToken token) { try { var buffer = new byte[1024 * 4]; WebSocketReceiveResult result; while (true) { Array.Clear(buffer); result = await ws.ReceiveAsync(buffer, token); if (result.CloseStatus.HasValue) break; var echo = new ArraySegment(buffer, 0, result.Count); await ws.SendAsync(echo, result.MessageType, result.EndOfMessage, token); } await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, token); } catch (Exception e) { Console.WriteLine(e); } } protected static async Task MessageAsync(WebSocket webSocket, CancellationToken token) { try { var buffer = new byte[1024 * 4]; var bytes = Encoding.UTF8.GetBytes("chocolate"); var result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); while (!result.CloseStatus.HasValue) { await webSocket.SendAsync(new ArraySegment(bytes), result.MessageType, result.EndOfMessage, token); result = await webSocket.ReceiveAsync(new ArraySegment(buffer), token); } await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, token); } catch (Exception e) { Console.WriteLine(e); } } protected async Task StartClient(Uri url) { var client = _factory.CreateClient(); await client.ConnectAsync(url, CancellationToken.None); var sending = Task.Run(async () => { var line = "test"; for (var i = 0; i < 10; i++) { var bytes = Encoding.UTF8.GetBytes(line); await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None); await Task.Delay(10); } await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); }); var receiving = Task.Run(async () => { var buffer = new byte[1024 * 4]; while (true) { var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Text) { _firstRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); } else if (result.MessageType == WebSocketMessageType.Close) { if (client.State != WebSocketState.Closed) { // Last version, the client state is CloseReceived // Valid states are: Open, CloseReceived, CloseSent await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } break; } } }); await Task.WhenAll(sending, receiving); } protected async Task StartSecondClient(Uri url) { await Task.Delay(500); var client = _factory.CreateClient(); await client.ConnectAsync(url, CancellationToken.None); var sending = Task.Run(async () => { var line = "test"; for (var i = 0; i < 10; i++) { var bytes = Encoding.UTF8.GetBytes(line); await client.SendAsync(new ArraySegment(bytes), WebSocketMessageType.Text, true, CancellationToken.None); await Task.Delay(10); } await client.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); }); var receiving = Task.Run(async () => { var buffer = new byte[1024 * 4]; while (true) { var result = await client.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Text) { _secondRecieved.Add(Encoding.UTF8.GetString(buffer, 0, result.Count)); } else if (result.MessageType == WebSocketMessageType.Close) { if (client.State != WebSocketState.Closed) { // Last version, the client state is CloseReceived // Valid states are: Open, CloseReceived, CloseSent await client.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None); } break; } } }); await Task.WhenAll(sending, receiving); } protected Task WhenIStartTheClients(int port) { var url = new UriBuilder(Uri.UriSchemeWs, "localhost", port).Uri; var firstClient = StartClient(url); var secondClient = StartSecondClient(url); return Task.WhenAll(firstClient, secondClient); } protected Task StartOcelotWithWebSockets(int port, Action configureServices) => StartOcelotWithWebSockets(port, Uri.UriSchemeHttp, configureServices); protected Task StartOcelotWithWebSockets(int port, string scheme, Action configureServices) { var url = DownstreamUrl(port, scheme); void ConfigureWebHost(IWebHostBuilder b) => b .UseUrls(url) .ConfigureLogging(WithConsole); return GivenOcelotHostIsRunning(WithBasicConfiguration, configureServices ?? WithAddOcelot, WithWebSockets, null, ConfigureWebHost, null, null); } protected static void WithWebSockets(IApplicationBuilder app) => app.UseWebSockets().UseOcelot().Wait(); protected static void WithHttp2(ListenOptions options) { options.Protocols = HttpProtocols.Http2; options.UseHttps("mycert2.pfx", "password"); } protected Task StartHttp2OcelotWithWebSockets(int port) { void WithOptions(KestrelServerOptions o) => o.ListenAnyIP(port, WithHttp2); var url = DownstreamUrl(port, Uri.UriSchemeHttps); void ConfigureWebHost(IWebHostBuilder b) => b.UseUrls(url) .ConfigureLogging(WithConsole) .ConfigureKestrel(WithOptions).UseKestrel(); // UseKestrelHttpsConfiguration() return GivenOcelotHostIsRunning(WithBasicConfiguration, WithAddOcelot, WithWebSockets, null, ConfigureWebHost, null, null); } } ================================================ FILE: test/Ocelot.AcceptanceTests/appsettings.json ================================================ { "Logging": { "IncludeScopes": true, "LogLevel": { "Default": "Error", "System": "Error", "Microsoft": "Error" } }, "spring": { "application": { "name": "ocelot" } }, "eureka": { "client": { "serviceUrl": "http://127.0.0.1:8761/eureka/", "shouldRegisterWithEureka": true, "shouldFetchRegistry": true, "port": 5000, "hostName": "localhost" } } } ================================================ FILE: test/Ocelot.AcceptanceTests/appsettings.product.json ================================================ { "Logging": { "IncludeScopes": true, "LogLevel": { "Default": "Error", "System": "Error", "Microsoft": "Error" } }, "spring": { "application": { "name": "product" } }, "eureka": { "client": { "serviceUrl": "http://localhost:8761/eureka/", "shouldRegisterWithEureka": true }, "instance": { "port": 50371 } } } ================================================ FILE: test/Ocelot.AcceptanceTests/mycert2.crt ================================================ -----BEGIN CERTIFICATE----- MIIEHTCCAwWgAwIBAgIUKXS7AlTRlVtDBeMDxEVUrptJ3a8wDQYJKoZIhvcNAQEL BQAwgZ0xCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlCZXJrc2hpcmUxEzARBgNVBAcM Ck1haWRlbmhlYWQxFTATBgNVBAoMDFRocmVlTWFtbWFsczEPMA0GA1UECwwGb3du ZXJzMRkwFwYDVQQDDBB0aHJlZW1hbW1hbHMuY29tMSIwIAYJKoZIhvcNAQkBFhNk b3RuZXQwNDRAZ21haWwuY29tMB4XDTI1MTIxNTE1NDQ0MVoXDTI4MDkxMDE1NDQ0 MVowgZ0xCzAJBgNVBAYTAlVLMRIwEAYDVQQIDAlCZXJrc2hpcmUxEzARBgNVBAcM Ck1haWRlbmhlYWQxFTATBgNVBAoMDFRocmVlTWFtbWFsczEPMA0GA1UECwwGb3du ZXJzMRkwFwYDVQQDDBB0aHJlZW1hbW1hbHMuY29tMSIwIAYJKoZIhvcNAQkBFhNk b3RuZXQwNDRAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAp873B/a12uQgr8GLcceJCc5xQkGI7f5Zg7brhBpycQymP6bYs3+BNgetbsWL mGbXWs1xvbUXghYxziu/0sxo1aFvrcSO84FfyjCkWc2jxrcLf/VosKobhd+Jo175 EvnSUz+VapSWJKXWUSlEHmnGaE9FKFfs99/+PfHemJHixyVOg7ekc9kzVZ2/Fwn5 Ej7SS1PHB8vVuTZerztSyOgeGm3Kqh2+5kn7q48uIVRn9O2FaRh4MaSKZnYLM6T4 gkdzctEjdqB0Tls+OqK/uAJ1hHJByoBhn1/b/meezp9jgLz/pUij3YI2zaBIA4Eg jjzhUm8X44bL8ptSJVmEs+4HvwIDAQABo1MwUTAdBgNVHQ4EFgQUla6gxlMjuzI/ FzdPuiA47ICdFB0wHwYDVR0jBBgwFoAUla6gxlMjuzI/FzdPuiA47ICdFB0wDwYD VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAR9vrWaZzTbL6gL422SLu IY3Y5w84uKN8QE5WpnbV1fOE5lq+fwHCsLwrz5dqXJZCuy5fAfrPvFCKbLE30DRU wN7ycXPVfInRA2xnqE8BPllyd110xCGWLh5/2cvSqPbcu+U1VTjkvAWzKz2VS3OT RPYhyp9/lyiMoq813rzIykhFHYYfuDBig1R8qoYRlGXWn7mTdttjouzRdBBce3BS mtQWpwSNBbheWq2GN5RzKo42+atW25jF7HaAKHzgAdqbBb3tQo9YN0BcTlR44IIZ eTzf9UWCqu3zmtHvrX74JU2TPfE9L/sZlN9Dh/fHDwc8IIEDxP624P3x6V77c7Dt QA== -----END CERTIFICATE----- ================================================ FILE: test/Ocelot.AcceptanceTests/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Serilog": { "type": "Direct", "requested": "[4.3.1, )", "resolved": "4.3.1", "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "TestStack.BDDfy": { "type": "Direct", "requested": "[8.0.9.120-beta, )", "resolved": "8.0.9.120-beta", "contentHash": "Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==" }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.AspNetCore.TestHost": "[10.0.5, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )" } } }, "net10.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net10.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net8.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Direct", "requested": "[8.0.25, )", "resolved": "8.0.25", "contentHash": "nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[8.0.25, )", "resolved": "8.0.25", "contentHash": "tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==", "dependencies": { "System.IO.Pipelines": "8.0.0" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Serilog": { "type": "Direct", "requested": "[4.3.1, )", "resolved": "4.3.1", "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "TestStack.BDDfy": { "type": "Direct", "requested": "[8.0.9.120-beta, )", "resolved": "8.0.9.120-beta", "contentHash": "Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==" }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "8.0.0" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==", "dependencies": { "System.Text.Encodings.Web": "10.0.0", "System.Text.Json": "10.0.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==", "dependencies": { "Microsoft.IdentityModel.Protocols": "7.1.2", "System.IdentityModel.Tokens.Jwt": "7.1.2" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.AspNetCore.TestHost": "[8.0.25, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net8.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net8.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Direct", "requested": "[9.0.14, )", "resolved": "9.0.14", "contentHash": "CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[9.0.14, )", "resolved": "9.0.14", "contentHash": "4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==" }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Serilog": { "type": "Direct", "requested": "[4.3.1, )", "resolved": "4.3.1", "contentHash": "savYe7h5yRlkqBVOwP8cIRDOdqKiPmYCU4W87JH38sBmcKD5EBoXvQIw6bNEvZ/pTe1gsiye3VFCzBsoppGkXQ==" }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Direct", "requested": "[3.3.0, )", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "TestStack.BDDfy": { "type": "Direct", "requested": "[8.0.9.120-beta, )", "resolved": "8.0.9.120-beta", "contentHash": "Q9dTKcuZd6uI5Pehcd0x851rhCxCH7nYIRK70ClMPsvCobcoiJIINGNI4Kiyzts1JVHGLykIaeBvLwHGbfQ4mA==" }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "9.0.3" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==", "dependencies": { "System.Text.Encodings.Web": "10.0.0", "System.Text.Json": "10.0.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==", "dependencies": { "Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Diagnostics": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Polly": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.AspNetCore.TestHost": "[9.0.14, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net9.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } } } } ================================================ FILE: test/Ocelot.Benchmarks/AllTheThingsBenchmarks.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Net; namespace Ocelot.Benchmarks; [Config(typeof(AllTheThingsBenchmarks))] public sealed class AllTheThingsBenchmarks : ManualConfig, IDisposable { private readonly BenchmarkSteps steps = new(); public void Dispose() => steps.Dispose(); public AllTheThingsBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { var port = PortFinder.GetRandomPort(); var route = steps.GivenDefaultRoute(port); var configuration = steps.GivenConfiguration(route); steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(AllTheThingsBenchmarks)); steps.GivenThereIsAConfiguration(configuration); steps.GivenOcelotIsRunning( host => host.ConfigureLogging( (c, l) => l.AddConfiguration(c.Configuration.GetSection("Logging"))) // Action postConfigureHost, ); } [Benchmark(Baseline = true)] public Task Baseline() { return steps.WhenIGetUrlOnTheApiGateway("/"); } /* Summary BenchmarkDotNet = v0.10.13, OS = macOS 10.12.6 (16G1212) [Darwin 16.7.0] Intel Core i5-4278U CPU 2.60GHz(Haswell), 1 CPU, 4 logical cores and 2 physical cores .NET Core SDK = 2.1.4 [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | Scaled | Gen 0 | Gen 1 | Allocated | --------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|------:|-------:|--------:|-------:|----------:| Baseline | 2.102 ms | 0.0292 ms | 0.0273 ms | 0.0070 ms | 2.063 ms | 2.080 ms | 2.093 ms | 2.122 ms | 2.152 ms | 475.8 | 1.00 | 31.2500 | 3.9063 | 1.63 KB | */ } ================================================ FILE: test/Ocelot.Benchmarks/BenchmarkSteps.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using System.Runtime.CompilerServices; namespace Ocelot.Benchmarks; internal class BenchmarkSteps : AcceptanceSteps { public void GivenThereIsAServiceRunningOnKestrel(int port, string basePath, int statusCode, Action configureKestrel, [CallerMemberName] string responseBody = null) => handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, configureKestrel, context => { context.Response.StatusCode = statusCode; return context.Response.WriteAsync(responseBody); }); public void GivenThereIsAServiceRunningOnKestrel(int port, string basePath, Action configureKestrel, RequestDelegate @delegate) => handler.GivenThereIsAServiceRunningOnWithKestrelOptions(DownstreamUrl(port), basePath, configureKestrel, @delegate); public int GivenOcelotIsRunning(Action postConfigureHost) => GivenOcelotIsRunning(null, null, null, null, postConfigureHost, null, null); public int GivenOcelotIsRunning(Action configureApp, Action postConfigureHost) => GivenOcelotIsRunning(null, null, configureApp, null, postConfigureHost, null, null); } ================================================ FILE: test/Ocelot.Benchmarks/DownstreamRouteFinderMiddlewareBenchmarks.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.DependencyInjection; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.Logging; using Ocelot.Middleware; namespace Ocelot.Benchmarks; [SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 5)] [Config(typeof(DownstreamRouteFinderMiddlewareBenchmarks))] public class DownstreamRouteFinderMiddlewareBenchmarks : ManualConfig { private DownstreamRouteFinderMiddleware _middleware; private RequestDelegate _next; private HttpContext _httpContext; public DownstreamRouteFinderMiddlewareBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { var serviceCollection = new ServiceCollection(); var config = new ConfigurationRoot(new List()); var builder = new OcelotBuilder(serviceCollection, config); var services = serviceCollection.BuildServiceProvider(true); var loggerFactory = services.GetService(); var drpf = services.GetService(); _next = async context => { await Task.CompletedTask; throw new Exception("BOOM"); }; _middleware = new DownstreamRouteFinderMiddleware(_next, loggerFactory, drpf); var httpContext = new DefaultHttpContext { Request = { Path = new PathString("/test"), QueryString = new QueryString("?a=b"), }, }; httpContext.Request.Headers.Append("Host", "most"); httpContext.Items.SetIInternalConfiguration(new InternalConfiguration()); _httpContext = httpContext; } [Benchmark(Baseline = true)] public async Task Baseline() { await _middleware.Invoke(_httpContext); } } ================================================ FILE: test/Ocelot.Benchmarks/ExceptionHandlerMiddlewareBenchmarks.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Errors.Middleware; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; namespace Ocelot.Benchmarks; [SimpleJob(launchCount: 1, warmupCount: 2, iterationCount: 5)] [Config(typeof(ExceptionHandlerMiddlewareBenchmarks))] public class ExceptionHandlerMiddlewareBenchmarks : ManualConfig { private ExceptionHandlerMiddleware _middleware; private RequestDelegate _next; private HttpContext _httpContext; public ExceptionHandlerMiddlewareBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { var serviceCollection = new ServiceCollection(); var config = new ConfigurationRoot(new List()); var builder = new OcelotBuilder(serviceCollection, config); var services = serviceCollection.BuildServiceProvider(true); var loggerFactory = services.GetService(); var repo = services.GetService(); _next = async context => { await Task.CompletedTask; throw new Exception("BOOM"); }; _middleware = new ExceptionHandlerMiddleware(_next, loggerFactory, repo); _httpContext = new DefaultHttpContext(); } [Benchmark(Baseline = true)] public async Task Baseline() { await _middleware.Invoke(_httpContext); } } ================================================ FILE: test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs ================================================ using BenchmarkDotNet.Order; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Ocelot.Logging; using Ocelot.Middleware; using System.Net; namespace Ocelot.Benchmarks; [Config(typeof(MsLoggerBenchmarks))] [Orderer(SummaryOrderPolicy.FastestToSlowest)] [MaxIterationCount(16)] public sealed class MsLoggerBenchmarks : ManualConfig, IDisposable { private readonly BenchmarkSteps steps = new(); public void Dispose() => steps.Dispose(); public MsLoggerBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } private Task SendRequest() { return steps.WhenIGetUrlOnTheApiGateway("/"); } [Benchmark(Baseline = true)] public async Task LogLevelCritical() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelCritical))] public void SetUpCritical() => OcelotFactory(LogLevel.Critical); [Benchmark] public async Task LogLevelError() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelError))] public void SetupError() => OcelotFactory(LogLevel.Error); [Benchmark] public async Task LogLevelWarning() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelWarning))] public void SetUpWarning() => OcelotFactory(LogLevel.Warning); [Benchmark] public async Task LogLevelInformation() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelInformation))] public void SetUpInformation() => OcelotFactory(LogLevel.Information); [Benchmark] public async Task LogLevelTrace() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelTrace))] public void SetUpTrace() => OcelotFactory(LogLevel.Trace); [GlobalCleanup(Targets = new[] { nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), nameof(LogLevelTrace), })] public void OcelotCleanup() { steps.Dispose(); } [GlobalCleanup] public void Cleanup() { Dispose(); } private void GivenOcelotIsRunning(LogLevel minLogLevel) => steps.GivenOcelotIsRunning( async app => { app.Use(async (context, next) => { var loggerFactory = context.RequestServices.GetService(); var ocelotLogger = loggerFactory.CreateLogger(); ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); await next.Invoke(); }); await app.UseOcelot(); }, // Action configureApp host => host.ConfigureLogging( (c, l) => l.ClearProviders().SetMinimumLevel(minLogLevel).AddConsole()) // Action postConfigureHost ); private void OcelotFactory(LogLevel minLogLevel) { var port = PortFinder.GetRandomPort(); var route = steps.GivenDefaultRoute(port); var configuration = steps.GivenConfiguration(route); steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(MsLoggerBenchmarks)); steps.GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(minLogLevel); } } ================================================ FILE: test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj ================================================  0.0.0-dev net8.0;net9.0;net10.0 disable disable false true Ocelot.Benchmarks Exe win-x64;osx-x64 false false false True ..\..\codeanalysis.ruleset $(NoWarn);CS0618;CS1591 ================================================ FILE: test/Ocelot.Benchmarks/PayloadBenchmarks.cs ================================================ using BenchmarkDotNet.Order; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Net.Http.Headers; using System.Reflection; using System.Text; namespace Ocelot.Benchmarks; [Config(typeof(PayloadBenchmarks))] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public sealed class PayloadBenchmarks : ManualConfig, IDisposable { private readonly BenchmarkSteps steps = new(); public void Dispose() => steps.Dispose(); private const string BasePayload = "{\"_id\":\"65789c1611a3b1feb49f9e65\",\"index\":0,\"guid\":\"6622d724-c17d-4939-9c68-158bf2dc5c57\",\"isActive\":false,\"balance\":\"$1,398.26\",\"picture\":\"http://placehold.it/32x32\",\"age\":33,\"eyeColor\":\"blue\",\"name\":\"WilkersonPayne\",\"gender\":\"male\",\"company\":\"NEOCENT\",\"email\":\"wilkersonpayne@neocent.com\",\"phone\":\"+1(837)588-3248\",\"address\":\"932BatchelderStreet,Campo,Texas,1310\",\"about\":\"Dolorsuntminimnullatemporlaboretempornostrudnon.Irureconsectetursintenimestadduissunttemporquisnisi.Laboreoccaecatculpaaliquaipsumreprehenderitadofficia.Sunteuutinpariaturanimofficia.CommodosintLoremametincididuntvelitesse.Nonaliquasintdoeiusmodexercitation.Suntcommododolorcupidatatculpareprehenderitfugiatexquisamet.\\r\\n\",\"registered\":\"2021-09-06T11:54:41-02:00\",\"latitude\":-45.256336,\"longitude\":164.343713,\"tags\":[\"cillum\",\"cupidatat\",\"aliquip\",\"culpa\",\"non\",\"laboris\",\"non\"],\"friends\":[{\"id\":0,\"name\":\"MistyMorton\"},{\"id\":1,\"name\":\"AraceliAcosta\"},{\"id\":2,\"name\":\"WalterDelaney\"}],\"greeting\":\"Hello,WilkersonPayne!Youhave1unreadmessages.\",\"favoriteFruit\":\"strawberry\"}"; public PayloadBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { var port = PortFinder.GetRandomPort(); var route = steps.GivenDefaultRoute(port); var configuration = steps.GivenConfiguration(route); steps.GivenThereIsAServiceRunningOnKestrel(port, "/", 201, options => options.Limits.MaxRequestBodySize = 2684354561, nameof(PayloadBenchmarks)); steps.GivenThereIsAConfiguration(configuration); steps.GivenOcelotIsRunning(host => host .ConfigureKestrel((_, options) => options.Limits.MaxRequestBodySize = 2684354561) .ConfigureLogging((hosting, logging) => logging.AddConfiguration(hosting.Configuration.GetSection("Logging")))); } [Benchmark(Baseline = true)] [ArgumentsSource(nameof(Payloads))] public async Task Baseline(string payLoadPath, string payloadName, bool isJson) { using var content = new StreamContent(File.OpenRead(payLoadPath)); content.Headers.ContentType = new MediaTypeHeaderValue(string.Concat("application/", isJson ? "json" : "octet-stream")); await steps.WhenIPostUrlOnTheApiGateway("/", content); } /// /// Generating the payloads for the benchmarks dynamically. /// /// The payloads containing path, file name and a boolean indicating if the file is a json or not. public static IEnumerable Payloads() { var baseDirectory = GetBaseDirectory(); var payloadsDirectory = Path.Combine(baseDirectory, nameof(Payloads)); if (!Directory.Exists(payloadsDirectory)) { Directory.CreateDirectory(payloadsDirectory); } // Array of sizes in kilobytes for JSON files var jsonSizes = new[] { 1, 16, 32, 64, 128, 256, 512, 2 * 1024, 8 * 1024, 15 * 1024, 30 * 1024 }; foreach (var size in jsonSizes) { yield return GeneratePayload(size, payloadsDirectory, $"{size}KBPayload.json", true); } // Array of sizes in megabytes for DAT files var datSizes = new[] { 10, 100, 1024 }; foreach (var size in datSizes) { yield return GeneratePayload(size, payloadsDirectory, $"{size}MBPayload.dat", false); } } private static string GetBaseDirectory() { var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); Debug.Assert(baseDirectory != null, nameof(baseDirectory) + " != null"); return baseDirectory; } private static object[] GeneratePayload(int size, string directory, string fileName, bool isJson) { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func) GenerateDummyJsonFile : GenerateDummyDatFile; return new object[] { generateDummy(size, filePath), fileName, isJson, }; } /// /// Generates a dummy payload of the given size in KB. /// The payload is a JSON array of the given size. /// /// The size in KB. /// The payload path. /// The current payload path. private static string GenerateDummyJsonFile(int sizeInKb, string payloadPath) { ArgumentNullException.ThrowIfNull(payloadPath); if (File.Exists(payloadPath)) { return payloadPath; } var targetSizeInBytes = sizeInKb * 1024L; using var fileStream = new FileStream(payloadPath, FileMode.Create, FileAccess.Write); using var streamWriter = new StreamWriter(fileStream); var byteArrayLength = Encoding.UTF8.GetBytes(BasePayload).Length; var firstObject = true; streamWriter.Write("["); while (fileStream.Length < targetSizeInBytes - byteArrayLength) { if (!firstObject) { streamWriter.Write(","); } else { firstObject = false; } streamWriter.Write(BasePayload); } streamWriter.Write("]"); return payloadPath; } /// /// Generates a dummy payload of the given size in MB. /// Avoiding maintaining a large file in the repository. /// /// The file size in MB. /// The path to the payload file. /// The payload file path. /// Throwing an exception if the payload path is null. private static string GenerateDummyDatFile(int sizeInMb, string payloadPath) { ArgumentNullException.ThrowIfNull(payloadPath); if (File.Exists(payloadPath)) { return payloadPath; } using var newFile = new FileStream(payloadPath, FileMode.CreateNew); newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin); newFile.WriteByte(0); newFile.Close(); return payloadPath; } } ================================================ FILE: test/Ocelot.Benchmarks/Program.cs ================================================ using BenchmarkDotNet.Running; namespace Ocelot.Benchmarks; public class Program { public static void Main(string[] args) { var switcher = new BenchmarkSwitcher( new[] { typeof(UrlPathToUrlPathTemplateMatcherBenchmarks), typeof(AllTheThingsBenchmarks), typeof(ExceptionHandlerMiddlewareBenchmarks), typeof(DownstreamRouteFinderMiddlewareBenchmarks), typeof(SerilogBenchmarks), typeof(MsLoggerBenchmarks), typeof(PayloadBenchmarks), typeof(ResponseBenchmarks), }); switcher.Run(args); } } ================================================ FILE: test/Ocelot.Benchmarks/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following set of attributes. // Change these attribute values to modify the information associated with an assembly. [assembly: AssemblyCompany("Three Mammals")] [assembly: AssemblyCopyright("© 2026 Three Mammals. MIT licensed OSS.")] [assembly: AssemblyProduct("Ocelot Gateway")] [assembly: AssemblyTrademark("Ocelot")] // Setting ComVisible to false makes the types in this assembly not visible to COM components. // If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("106b49e6-95f6-4a7b-b81c-96bfa74af035")] ================================================ FILE: test/Ocelot.Benchmarks/ResponseBenchmarks.cs ================================================ using BenchmarkDotNet.Order; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Net.Http.Headers; using System.Reflection; using System.Text; namespace Ocelot.Benchmarks; [Config(typeof(ResponseBenchmarks))] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public sealed class ResponseBenchmarks : ManualConfig, IDisposable { private string _currentPayloadPath; private bool _currentIsJson; private const string BasePayload = "{\"_id\":\"65789c1611a3b1feb49f9e65\",\"index\":0,\"guid\":\"6622d724-c17d-4939-9c68-158bf2dc5c57\",\"isActive\":false,\"balance\":\"$1,398.26\",\"picture\":\"http://placehold.it/32x32\",\"age\":33,\"eyeColor\":\"blue\",\"name\":\"WilkersonPayne\",\"gender\":\"male\",\"company\":\"NEOCENT\",\"email\":\"wilkersonpayne@neocent.com\",\"phone\":\"+1(837)588-3248\",\"address\":\"932BatchelderStreet,Campo,Texas,1310\",\"about\":\"Dolorsuntminimnullatemporlaboretempornostrudnon.Irureconsectetursintenimestadduissunttemporquisnisi.Laboreoccaecatculpaaliquaipsumreprehenderitadofficia.Sunteuutinpariaturanimofficia.CommodosintLoremametincididuntvelitesse.Nonaliquasintdoeiusmodexercitation.Suntcommododolorcupidatatculpareprehenderitfugiatexquisamet.\\r\\n\",\"registered\":\"2021-09-06T11:54:41-02:00\",\"latitude\":-45.256336,\"longitude\":164.343713,\"tags\":[\"cillum\",\"cupidatat\",\"aliquip\",\"culpa\",\"non\",\"laboris\",\"non\"],\"friends\":[{\"id\":0,\"name\":\"MistyMorton\"},{\"id\":1,\"name\":\"AraceliAcosta\"},{\"id\":2,\"name\":\"WalterDelaney\"}],\"greeting\":\"Hello,WilkersonPayne!Youhave1unreadmessages.\",\"favoriteFruit\":\"strawberry\"}"; private readonly BenchmarkSteps steps = new(); public void Dispose() => steps.Dispose(); public ResponseBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { var port = PortFinder.GetRandomPort(); var route = steps.GivenDefaultRoute(port); var configuration = steps.GivenConfiguration(route); GivenThereIsAServiceRunningOn(port, "/", 201); steps.GivenThereIsAConfiguration(configuration); steps.GivenOcelotIsRunning(host => host .ConfigureKestrel((_, options) => options.Limits.MaxRequestBodySize = 2684354561) .ConfigureLogging((hosting, logging) => logging.AddConfiguration(hosting.Configuration.GetSection("Logging")))); } [Benchmark(Baseline = true)] [ArgumentsSource(nameof(Payloads))] public Task Baseline(string payLoadPath, string payloadName, bool isJson) { _currentPayloadPath = payLoadPath; _currentIsJson = isJson; return steps.WhenIGetUrlOnTheApiGateway("/"); } /// /// Generating the payloads for the benchmarks dynamically. /// /// The payloads containing path, file name and a boolean indicating if the file is a json or not. public static IEnumerable Payloads() { var baseDirectory = GetBaseDirectory(); var payloadsDirectory = Path.Combine(baseDirectory, nameof(Payloads)); if (!Directory.Exists(payloadsDirectory)) { Directory.CreateDirectory(payloadsDirectory); } // Array of sizes in kilobytes for JSON files var jsonSizes = new[] { 1, 16, 32, 64, 128, 256, 512, 2 * 1024, 8 * 1024, 15 * 1024, 30 * 1024 }; foreach (var size in jsonSizes) { yield return GeneratePayload(size, payloadsDirectory, $"{size}KBPayload.json", true); } // Array of sizes in megabytes for DAT files var datSizes = new[] { 10, 100, 1024 }; foreach (var size in datSizes) { yield return GeneratePayload(size, payloadsDirectory, $"{size}MBPayload.dat", false); } } private static string GetBaseDirectory() { var baseDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); Debug.Assert(baseDirectory != null, nameof(baseDirectory) + " != null"); return baseDirectory; } private static object[] GeneratePayload(int size, string directory, string fileName, bool isJson) { var filePath = Path.Combine(directory, fileName); var generateDummy = isJson ? (Func)GenerateDummyJsonFile : GenerateDummyDatFile; return new object[] { generateDummy(size, filePath), fileName, isJson, }; } /// /// Generates a dummy payload of the given size in KB. /// The payload is a JSON array of the given size. /// /// The size in KB. /// The payload path. /// The current payload path. private static string GenerateDummyJsonFile(int sizeInKb, string payloadPath) { ArgumentNullException.ThrowIfNull(payloadPath); if (File.Exists(payloadPath)) { return payloadPath; } var targetSizeInBytes = sizeInKb * 1024L; using var fileStream = new FileStream(payloadPath, FileMode.Create, FileAccess.Write); using var streamWriter = new StreamWriter(fileStream); var byteArrayLength = Encoding.UTF8.GetBytes(BasePayload).Length; var firstObject = true; streamWriter.Write("["); while (fileStream.Length < targetSizeInBytes - byteArrayLength) { if (!firstObject) { streamWriter.Write(","); } else { firstObject = false; } streamWriter.Write(BasePayload); } streamWriter.Write("]"); return payloadPath; } /// /// Generates a dummy payload of the given size in MB. /// Avoiding maintaining a large file in the repository. /// /// The file size in MB. /// The path to the payload file. /// The payload file path. /// Throwing an exception if the payload path is null. private static string GenerateDummyDatFile(int sizeInMb, string payloadPath) { ArgumentNullException.ThrowIfNull(payloadPath); if (File.Exists(payloadPath)) { return payloadPath; } using var newFile = new FileStream(payloadPath, FileMode.CreateNew); newFile.Seek(sizeInMb * 1024L * 1024, SeekOrigin.Begin); newFile.WriteByte(0); newFile.Close(); return payloadPath; } private void GivenThereIsAServiceRunningOn(int port, string basePath, int statusCode) => steps.GivenThereIsAServiceRunningOnKestrel(port, basePath, options => options.Limits.MaxRequestBodySize = 2684354561, async context => { context.Response.StatusCode = statusCode; context.Response.ContentType = string.Concat("application/", _currentIsJson ? "json" : "octet-stream"); using var content = new StreamContent(File.OpenRead(_currentPayloadPath)); content.Headers.ContentType = new MediaTypeHeaderValue(string.Concat("application/", _currentIsJson ? "json" : "octet-stream")); await content.CopyToAsync(context.Response.Body); }); } ================================================ FILE: test/Ocelot.Benchmarks/SerilogBenchmarks.cs ================================================ using BenchmarkDotNet.Order; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Ocelot.Logging; using Ocelot.Middleware; using Serilog; using Serilog.Core; using System.Net; namespace Ocelot.Benchmarks; [Config(typeof(SerilogBenchmarks))] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public sealed class SerilogBenchmarks : ManualConfig, IDisposable { private Logger _logger; private readonly BenchmarkSteps steps = new(); public void Dispose() => steps.Dispose(); public SerilogBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } private Task SendRequest() { return steps.WhenIGetUrlOnTheApiGateway("/"); } [Benchmark(Baseline = true)] public async Task LogLevelCritical() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelCritical))] public void SetUpCritical() => OcelotFactory(LogLevel.Critical); [Benchmark] public async Task LogLevelError() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelError))] public void SetupError() => OcelotFactory(LogLevel.Error); [Benchmark] public async Task LogLevelWarning() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelWarning))] public void SetUpWarning() => OcelotFactory(LogLevel.Warning); [Benchmark] public async Task LogLevelInformation() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelInformation))] public void SetUpInformation() => OcelotFactory(LogLevel.Information); [Benchmark] public async Task LogLevelTrace() => await SendRequest(); [GlobalSetup(Target = nameof(LogLevelTrace))] public void SetUpTrace() => OcelotFactory(LogLevel.Trace); [GlobalCleanup(Targets = new[] { nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), nameof(LogLevelTrace), })] public void OcelotCleanup() { steps.Dispose(); } [GlobalCleanup] public void Cleanup() { Dispose(); } private void GivenOcelotIsRunning(LogLevel minLogLevel) { _logger = minLogLevel switch { LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), LogLevel.None => new LoggerConfiguration() .WriteTo.File( $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") .CreateLogger(), _ => throw new ArgumentOutOfRangeException(nameof(minLogLevel), minLogLevel, null), }; steps.GivenOcelotIsRunning( async app => await app .Use(async (context, next) => { var loggerFactory = context.RequestServices.GetService(); var ocelotLogger = loggerFactory.CreateLogger(); ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", new Exception("test")); await next.Invoke(); }) .UseOcelot(), // Action configureApp host => host.ConfigureLogging( (c, l) => l.ClearProviders().SetMinimumLevel(minLogLevel).AddSerilog(_logger)) // Action postConfigureHost ); } private void OcelotFactory(LogLevel minLogLevel) { var port = PortFinder.GetRandomPort(); var route = steps.GivenDefaultRoute(port); var configuration = steps.GivenConfiguration(route); steps.GivenThereIsAServiceRunningOn(port, HttpStatusCode.Created, nameof(SerilogBenchmarks)); steps.GivenThereIsAConfiguration(configuration); GivenOcelotIsRunning(minLogLevel); } } ================================================ FILE: test/Ocelot.Benchmarks/UrlPathToUrlPathTemplateMatcherBenchmarks.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.Benchmarks; [Config(typeof(UrlPathToUrlPathTemplateMatcherBenchmarks))] public class UrlPathToUrlPathTemplateMatcherBenchmarks : ManualConfig { private RegExUrlMatcher _urlPathMatcher; private UpstreamPathTemplate _pathTemplate; private string _downstreamUrlPath; public UrlPathToUrlPathTemplateMatcherBenchmarks() { AddColumn(StatisticColumn.AllStatistics); AddDiagnoser(MemoryDiagnoser.Default); AddValidator(BaselineValidator.FailOnError); } [GlobalSetup] public void SetUp() { _urlPathMatcher = new RegExUrlMatcher(); _pathTemplate = new UpstreamPathTemplate("api/product/products/{productId}/variants/", 0, false, null); _downstreamUrlPath = "api/product/products/1/variants/?soldout=false"; } [Benchmark(Baseline = true)] public void Baseline() { _urlPathMatcher.Match(_downstreamUrlPath, null, _pathTemplate); } // * Summary * // BenchmarkDotNet=v0.10.13, OS=macOS 10.12.6 (16G1212) [Darwin 16.7.0] // Intel Core i5-4278U CPU 2.60GHz (Haswell), 1 CPU, 4 logical cores and 2 physical cores // .NET Core SDK=2.1.4 // [Host] : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT // DefaultJob : .NET Core 2.0.6 (CoreCLR 4.6.0.0, CoreFX 4.6.26212.01), 64bit RyuJIT // Method | Mean | Error | StdDev | StdErr | Min | Q1 | Median | Q3 | Max | Op/s | // ----------- |---------:|----------:|----------:|----------:|---------:|---------:|---------:|---------:|---------:|----------:| // Benchmark1 | 3.133 us | 0.0492 us | 0.0460 us | 0.0119 us | 3.082 us | 3.100 us | 3.122 us | 3.168 us | 3.233 us | 319,161.9 | } ================================================ FILE: test/Ocelot.Benchmarks/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using BenchmarkDotNet.Attributes; global using BenchmarkDotNet.Columns; global using BenchmarkDotNet.Configs; global using BenchmarkDotNet.Diagnosers; global using BenchmarkDotNet.Validators; global using Ocelot; global using Ocelot.Testing; ================================================ FILE: test/Ocelot.Benchmarks/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "BenchmarkDotNet": { "type": "Direct", "requested": "[0.15.8, )", "resolved": "0.15.8", "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", "dependencies": { "BenchmarkDotNet.Annotations": "0.15.8", "CommandLineParser": "2.9.1", "Gee.External.Capstone": "2.3.0", "Iced": "1.21.0", "Microsoft.CodeAnalysis.CSharp": "4.14.0", "Microsoft.Diagnostics.Runtime": "3.1.512801", "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", "Microsoft.DotNet.PlatformAbstractions": "3.1.6", "Perfolizer": "[0.6.1]", "System.Management": "9.0.5" } }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "BenchmarkDotNet.Annotations": { "type": "Transitive", "resolved": "0.15.8", "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "CommandLineParser": { "type": "Transitive", "resolved": "2.9.1", "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "Iced": { "type": "Transitive", "resolved": "1.21.0", "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Transitive", "resolved": "3.11.0", "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" }, "Microsoft.CodeAnalysis.Common": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0" } }, "Microsoft.CodeAnalysis.CSharp": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0", "Microsoft.CodeAnalysis.Common": "[4.14.0]" } }, "Microsoft.Diagnostics.NETCore.Client": { "type": "Transitive", "resolved": "0.2.510501", "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", "dependencies": { "Microsoft.Extensions.Logging": "6.0.0" } }, "Microsoft.Diagnostics.Runtime": { "type": "Transitive", "resolved": "3.1.512801", "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" } }, "Microsoft.Diagnostics.Tracing.TraceEvent": { "type": "Transitive", "resolved": "3.1.21", "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", "System.Reflection.TypeExtensions": "4.7.0" } }, "Microsoft.DotNet.PlatformAbstractions": { "type": "Transitive", "resolved": "3.1.6", "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.0.1" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==", "dependencies": { "Microsoft.IdentityModel.Logging": "8.0.1" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Perfolizer": { "type": "Transitive", "resolved": "0.6.1", "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", "dependencies": { "Pragmastat": "3.2.4" } }, "Pragmastat": { "type": "Transitive", "resolved": "3.2.4", "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" }, "Serilog": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Reflection.TypeExtensions": { "type": "Transitive", "resolved": "4.7.0", "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.AspNetCore.TestHost": "[10.0.5, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )" } } }, "net10.0/osx-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } } }, "net10.0/win-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } } }, "net8.0": { "BenchmarkDotNet": { "type": "Direct", "requested": "[0.15.8, )", "resolved": "0.15.8", "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", "dependencies": { "BenchmarkDotNet.Annotations": "0.15.8", "CommandLineParser": "2.9.1", "Gee.External.Capstone": "2.3.0", "Iced": "1.21.0", "Microsoft.CodeAnalysis.CSharp": "4.14.0", "Microsoft.Diagnostics.Runtime": "3.1.512801", "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", "Microsoft.DotNet.PlatformAbstractions": "3.1.6", "Perfolizer": "[0.6.1]", "System.Management": "9.0.5" } }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "BenchmarkDotNet.Annotations": { "type": "Transitive", "resolved": "0.15.8", "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "CommandLineParser": { "type": "Transitive", "resolved": "2.9.1", "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "Iced": { "type": "Transitive", "resolved": "1.21.0", "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==", "dependencies": { "System.IO.Pipelines": "8.0.0" } }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Transitive", "resolved": "3.11.0", "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" }, "Microsoft.CodeAnalysis.Common": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0", "System.Collections.Immutable": "9.0.0", "System.Reflection.Metadata": "9.0.0" } }, "Microsoft.CodeAnalysis.CSharp": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0", "Microsoft.CodeAnalysis.Common": "[4.14.0]", "System.Collections.Immutable": "9.0.0", "System.Reflection.Metadata": "9.0.0" } }, "Microsoft.Diagnostics.NETCore.Client": { "type": "Transitive", "resolved": "0.2.510501", "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", "dependencies": { "Microsoft.Extensions.Logging": "6.0.0" } }, "Microsoft.Diagnostics.Runtime": { "type": "Transitive", "resolved": "3.1.512801", "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" } }, "Microsoft.Diagnostics.Tracing.TraceEvent": { "type": "Transitive", "resolved": "3.1.21", "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.510501" } }, "Microsoft.DotNet.PlatformAbstractions": { "type": "Transitive", "resolved": "3.1.6", "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==", "dependencies": { "System.Text.Encodings.Web": "10.0.0", "System.Text.Json": "10.0.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "33eTIA2uO/L9utJjZWbKsMSVsQf7F8vtd6q5mQX7ZJzNvCpci5fleD6AeANGlbbb7WX7XKxq9+Dkb5e3GNDrmQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "cloLGeZolXbCJhJBc5OC05uhrdhdPL6MWHuVUnkkUvPDeK7HkwThBaLZ1XjBQVk9YhxXE2OvHXnKi0PLleXxDg==", "dependencies": { "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "YCxBt2EeJP8fcXk9desChkWI+0vFqFLvBwrz5hBMsoh0KJE6BC66DnzkdzkJNqMltLromc52dkdT206jJ38cTw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "7.1.2" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==", "dependencies": { "Microsoft.IdentityModel.Protocols": "7.1.2", "System.IdentityModel.Tokens.Jwt": "7.1.2" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "oICJMqr3aNEDZOwnH5SK49bR6Z4aX0zEAnOLuhloumOSuqnNq+GWBdQyrgILnlcT5xj09xKCP/7Y7gJYB+ls/g==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Perfolizer": { "type": "Transitive", "resolved": "0.6.1", "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", "dependencies": { "Pragmastat": "3.2.4" } }, "Pragmastat": { "type": "Transitive", "resolved": "3.2.4", "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" }, "Serilog": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" }, "System.Collections.Immutable": { "type": "Transitive", "resolved": "9.0.0", "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "0KdBK+h7G13PuOSC2R/DalAoFMvdYMznvGRuICtkdcUMXgl/gYXsG6z4yUvTxHSMACorWgHCU1Faq0KUHU6yAQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "Thhbe1peAmtSBFaV/ohtykXiZSOkx59Da44hvtWfIMFofDA3M3LaVyjstACf2rKGn4dEDR2cUpRAZ0Xs/zB+7Q==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Reflection.Metadata": { "type": "Transitive", "resolved": "9.0.0", "contentHash": "ANiqLu3DxW9kol/hMmTWbt3414t9ftdIuiIU7j80okq2YzAueo120M442xk1kDJWtmZTqWQn7wHDvMRipVOEOQ==", "dependencies": { "System.Collections.Immutable": "9.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.AspNetCore.TestHost": "[8.0.25, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net8.0/osx-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net8.0/win-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0": { "BenchmarkDotNet": { "type": "Direct", "requested": "[0.15.8, )", "resolved": "0.15.8", "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", "dependencies": { "BenchmarkDotNet.Annotations": "0.15.8", "CommandLineParser": "2.9.1", "Gee.External.Capstone": "2.3.0", "Iced": "1.21.0", "Microsoft.CodeAnalysis.CSharp": "4.14.0", "Microsoft.Diagnostics.Runtime": "3.1.512801", "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", "Microsoft.DotNet.PlatformAbstractions": "3.1.6", "Perfolizer": "[0.6.1]", "System.Management": "9.0.5" } }, "Serilog.AspNetCore": { "type": "Direct", "requested": "[10.0.0, )", "resolved": "10.0.0", "contentHash": "a/cNa1mY4On1oJlfGG1wAvxjp5g7OEzk/Jf/nm7NF9cWoE7KlZw1GldrifUBWm9oKibHkR7Lg/l5jy3y7ACR8w==", "dependencies": { "Serilog": "4.3.0", "Serilog.Extensions.Hosting": "10.0.0", "Serilog.Formatting.Compact": "3.0.0", "Serilog.Settings.Configuration": "10.0.0", "Serilog.Sinks.Console": "6.1.1", "Serilog.Sinks.Debug": "3.0.0", "Serilog.Sinks.File": "7.0.0" } }, "BenchmarkDotNet.Annotations": { "type": "Transitive", "resolved": "0.15.8", "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "CommandLineParser": { "type": "Transitive", "resolved": "2.9.1", "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "Iced": { "type": "Transitive", "resolved": "1.21.0", "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==" }, "Microsoft.CodeAnalysis.Analyzers": { "type": "Transitive", "resolved": "3.11.0", "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" }, "Microsoft.CodeAnalysis.Common": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0" } }, "Microsoft.CodeAnalysis.CSharp": { "type": "Transitive", "resolved": "4.14.0", "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", "dependencies": { "Microsoft.CodeAnalysis.Analyzers": "3.11.0", "Microsoft.CodeAnalysis.Common": "[4.14.0]" } }, "Microsoft.Diagnostics.NETCore.Client": { "type": "Transitive", "resolved": "0.2.510501", "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", "dependencies": { "Microsoft.Extensions.Logging": "6.0.0" } }, "Microsoft.Diagnostics.Runtime": { "type": "Transitive", "resolved": "3.1.512801", "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" } }, "Microsoft.Diagnostics.Tracing.TraceEvent": { "type": "Transitive", "resolved": "3.1.21", "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", "dependencies": { "Microsoft.Diagnostics.NETCore.Client": "0.2.510501" } }, "Microsoft.DotNet.PlatformAbstractions": { "type": "Transitive", "resolved": "3.1.6", "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "H4SWETCh/cC5L1WtWchHR6LntGk3rDTTznZMssr4cL8IbDmMWBxY+MOGDc/ASnqNolLKPIWHWeuC1ddiL/iNPw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "d2kDKnCsJvY7mBVhcjPSp9BkJk48DsaHPg5u+Oy4f8XaOqnEedRy/USyvnpHL92wpJ6DrTPy7htppUUzskbCXQ==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "tMF9wNh+hlyYDWB8mrFCQHQmWHlRosol1b/N2Jrefy1bFLnuTlgSYmPyHNmz8xVQgs7DpXytBRWxGhG+mSTp0g==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Configuration.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "f0RBabswJq+gRu5a+hWIobrLWiUYPKMhCD9WO3sYBAdSy3FFH14LMvLVFZc2kPSCimBLxSuitUhsd6tb0TAY6A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "L3AdmZ1WOK4XXT5YFPEwyt0ep6l8lGIPs7F5OOBZc77Zqeo01Of7XXICy47628sdVl0v/owxYJTe86DTgFwKCA==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "RFYJR7APio/BiqdQunRq6DB+nDB6nc2qhHr77mlvZ0q0BT8PubMXN7XicmfzCbrDE/dzhBnUKBRXLTcqUiZDGg==", "dependencies": { "System.Text.Encodings.Web": "10.0.0", "System.Text.Json": "10.0.0" } }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "/ppSdehKk3fuXjlqCDgSOtjRK/pSHU8eWgzSHfHdwVm5BP4Dgejehkw+PtxKG2j98qTDEHDst2Y99aNsmJldmw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "KrN6TGFwCwqOkLLk/idW/XtDQh+8In+CL9T4M1Dx+5ScsjTq4TlVbal8q532m82UYrMr6RiQJF2HvYCN0QwVsA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0" } }, "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "BStFkd5CcnEtarlcgYDBcFzGYCuuNMzPs02wN3WBsOFoYIEmYoUdAiU+au6opzoqfTYJsMTW00AeqDdnXH2CvA==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "FU/IfjDfwaMuKr414SSQNTIti/69bHEMb+QKrskRb26oVqpx3lNFXMjs/RC9ZUuhBhcwDM2BwOgoMw+PZ+beqQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "System.Diagnostics.DiagnosticSource": "10.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "8oCAgXOow5XDrY9HaXX1QmH3ORsyZO/ANVHBlhLyCeWTH5Sg4UuqZeOTWJi6484M+LqSx0RqQXDJtdYy2BNiLQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Primitives": "10.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "inRnbpCS0nwO/RuoZIAqxQUuyjaknOOnCEZB55KSMMjRhl0RQDttSmLSGsUJN3RQ3ocf5NDLFd2mOQViHqMK5w==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.0.1" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==", "dependencies": { "Microsoft.IdentityModel.Logging": "8.0.1" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Perfolizer": { "type": "Transitive", "resolved": "0.6.1", "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", "dependencies": { "Pragmastat": "3.2.4" } }, "Pragmastat": { "type": "Transitive", "resolved": "3.2.4", "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" }, "Serilog": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "+cDryFR0GRhsGOnZSKwaDzRRl4MupvJ42FhCE4zhQRVanX0Jpg6WuCBk59OVhVDPmab1bB+nRykAnykYELA9qQ==" }, "Serilog.Extensions.Hosting": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "E7juuIc+gzoGxgzFooFgAV8g9BfiSXNKsUok9NmEpyAXg2odkcPsMa/Yo4axkJRlh0se7mkYQ1GXDaBemR+b6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Hosting.Abstractions": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Serilog": "4.3.0", "Serilog.Extensions.Logging": "10.0.0" } }, "Serilog.Extensions.Logging": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "vx0kABKl2dWbBhhqAfTOk53/i8aV/5VaT3a6il9gn72Wqs2pM7EK2OB6No6xdqK2IaY6Zf9gdjLuK9BVa2rT+Q==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0", "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "LNq+ibS1sbhTqPV1FIE69/9AJJbfaOhnaqkzcjFy95o+4U+STsta9mi97f1smgXsWYKICDeGUf8xUGzd/52/uA==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyModel": "10.0.0", "Serilog": "4.3.0" } }, "Serilog.Sinks.Console": { "type": "Transitive", "resolved": "6.1.1", "contentHash": "8jbqgjUyZlfCuSTaJk6lOca465OndqOz3KZP6Cryt/IqZYybyBu7GP0fE/AXBzrrQB3EBmQntBFAvMVz1COvAA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", "resolved": "3.0.0", "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", "resolved": "7.0.0", "contentHash": "fKL7mXv7qaiNBUC71ssvn/dU0k9t0o45+qm2XgKAlSt19xF+ijjxyA3R6HmCgfKEKwfcfkwWjayuQtRueZFkYw==", "dependencies": { "Serilog": "4.2.0" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "0KdBK+h7G13PuOSC2R/DalAoFMvdYMznvGRuICtkdcUMXgl/gYXsG6z4yUvTxHSMACorWgHCU1Faq0KUHU6yAQ==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.AspNetCore.TestHost": "[9.0.14, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net9.0/osx-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0/win-x64": { "Gee.External.Capstone": { "type": "Transitive", "resolved": "2.3.0", "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw==" }, "System.Management": { "type": "Transitive", "resolved": "9.0.5", "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", "dependencies": { "System.CodeDom": "9.0.5" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } } } } ================================================ FILE: test/Ocelot.ManualTest/Actions/Basic.cs ================================================ using Ocelot.DependencyInjection; using Ocelot.Middleware; namespace Ocelot.ManualTest.Actions; public class Basic { internal static async Task RunAsync(string[] args) { Console.WriteLine("Starting Ocelot... "); var builder = WebApplication.CreateBuilder(args); builder.Configuration .SetBasePath(builder.Environment.ContentRootPath) .AddOcelot(); builder.Services //.AddAuthentication() //.AddJwtBearer("TestKey", x => //{ // x.Authority = "test"; // x.Audience = "test"; //}); //.AddSingleton((x, t, f) => new FakeHandler(f)) .AddOcelot(builder.Configuration); //.AddDelegatingHandler(true); //.AddAdministration("/administration", "secret"); builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging")); if (builder.Environment.IsDevelopment()) { builder.Logging.AddConsole(); } var app = builder.Build(); await app.UseOcelot(); //await app.UseOcelot(pipeline => //{ // pipeline.PreAuthenticationMiddleware = MetadataMiddleware.Invoke; //}); await app.RunAsync(); // Ocelot will exit from this method when pressing Ctrl+C } } ================================================ FILE: test/Ocelot.ManualTest/Actions/ManualTests.cs ================================================ namespace Ocelot.ManualTest.Actions; public class ManualTests { internal static void Run(string[] args) { Console.WriteLine("No tests to perform!"); } } ================================================ FILE: test/Ocelot.ManualTest/DelegatingHandlers/FakeHandler.cs ================================================ using Ocelot.Logging; namespace Ocelot.ManualTest.DelegatingHandlers; public class FakeHandler : DelegatingHandler { private readonly IOcelotLogger _logger; public FakeHandler(IOcelotLoggerFactory factory) { _logger = factory.CreateLogger(); } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { _logger.LogDebug(() => $"{nameof(request.RequestUri)}: {request.RequestUri}"); // Do stuff and optionally call the base handler.. return base.SendAsync(request, cancellationToken); } } ================================================ FILE: test/Ocelot.ManualTest/Dockerfile ================================================ #This is the base image used for any ran images FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base WORKDIR /app EXPOSE 80 #This image is used to build the source for the runnable app #It can also be used to run other CLI commands on the project, such as packing/deploying nuget packages. Some examples: #Run tests: docker build --target builder -t ocelot-build . && docker run ocelot-build test --logger:trx;LogFileName=results.trx #Run benchmarks: docker build --target builder --build-arg build_configuration=Release -t ocelot-build . && docker run ocelot-build run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS builder WORKDIR /build #First we add only the project files so that we can cache nuget packages with dotnet restore COPY Ocelot.sln Ocelot.sln COPY src/Ocelot/Ocelot.csproj src/Ocelot/Ocelot.csproj COPY src/Ocelot.Administration/Ocelot.Administration.csproj src/Ocelot.Administration/Ocelot.Administration.csproj COPY src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj src/Ocelot.Cache.CacheManager/Ocelot.Cache.CacheManager.csproj COPY src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj src/Ocelot.Provider.Consul/Ocelot.Provider.Consul.csproj COPY src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj src/Ocelot.Provider.Eureka/Ocelot.Provider.Eureka.csproj COPY src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj COPY src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj src/Ocelot.Tracing.Butterfly/Ocelot.Tracing.Butterfly.csproj COPY src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj src/Ocelot.Provider.Kubernetes/Ocelot.Provider.Kubernetes.csproj COPY test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj COPY test/Ocelot.ManualTest/Ocelot.ManualTest.csproj test/Ocelot.ManualTest/Ocelot.ManualTest.csproj COPY test/Ocelot.UnitTests/Ocelot.UnitTests.csproj test/Ocelot.UnitTests/Ocelot.UnitTests.csproj COPY test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj RUN dotnet restore #Now we add the rest of the source and run a complete build... --no-restore is used because nuget should be resolved at this point COPY codeanalysis.ruleset codeanalysis.ruleset COPY src src COPY test test ARG build_configuration=Debug RUN dotnet build --no-restore -c ${build_configuration} ENTRYPOINT ["dotnet"] #This is just for holding the published manual tests... FROM builder AS manual-test-publish ARG build_configuration=Debug RUN dotnet publish --no-build -c ${build_configuration} -o /app test/Ocelot.ManualTest #Run manual tests! This is the default run option. #docker build -t ocelot-manual-test . && docker run --net host ocelot-manual-test FROM base AS manual-test ENV ASPNETCORE_ENVIRONMENT=Development COPY --from=manual-test-publish /app . ENTRYPOINT ["dotnet", "Ocelot.ManualTest.dll"] ================================================ FILE: test/Ocelot.ManualTest/Middlewares/MetadataMiddleware.cs ================================================ using Ocelot.Logging; using Ocelot.Middleware; using System.Text.Json; namespace Ocelot.ManualTest.Middlewares; public class MetadataMiddleware { public static Task Invoke(HttpContext context, Func next) { var logger = GetLogger(context); var downstreamRoute = context.Items.DownstreamRoute(); if (downstreamRoute?.MetadataOptions?.Metadata is { } metadata) { logger.LogInformation(() => { var metadataInJson = JsonSerializer.Serialize(metadata, JsonSerializerOptions.Web); var message = $"{nameof(MetadataMiddleware)} found some metadata: {metadataInJson}"; return message; }); } return next(); } private static IOcelotLogger GetLogger(HttpContext context) { var loggerFactory = context.RequestServices.GetRequiredService(); return loggerFactory.CreateLogger(); } } ================================================ FILE: test/Ocelot.ManualTest/Ocelot.ManualTest.csproj ================================================  net8.0;net9.0;net10.0 enable enable false true Exe win-x64;osx-x64 True ..\..\codeanalysis.ruleset $(NoWarn);CS0618;CS1591 0.0.0-dev Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway © 2026 Three Mammals. MIT licensed OSS https://github.com/ThreeMammals/Ocelot/tree/main/test/Ocelot.ManualTest https://github.com/ThreeMammals/Ocelot.git LICENSE.md PreserveNewest PreserveNewest PreserveNewest ================================================ FILE: test/Ocelot.ManualTest/Ocelot.postman_collection.json ================================================ { "id": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "name": "Ocelot", "description": "", "order": [ "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", "c4494401-3985-a5bf-71fb-6e4171384ac6", "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", "e8825dc3-4137-99a7-0000-ef5786610dc3", "fddfc4fa-5114-69e3-4744-203ed71a526b", "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", "4684c2fa-f38c-c193-5f55-bf563a1978c6", "37bfa9f1-fe29-6a68-e558-66d125d2c96f", "5f308240-79e3-cf74-7a6b-fe462f0d54f1", "178f16da-c61b-c881-1c33-9d64a56851a4" ], "folders": [], "folders_order": [], "timestamp": 0, "owner": "212120", "public": false, "requests": [ { "folder": null, "id": "09af8dda-a9cb-20d2-5ee3-0a3023773a1a", "name": "GET http://localhost:5000/comments?postId=1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/comments?postId=1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "id": "178f16da-c61b-c881-1c33-9d64a56851a4", "headers": "Authorization: Bearer {{AccessToken}}\n", "headerData": [ { "key": "Authorization", "value": "Bearer {{AccessToken}}", "enabled": true, "description": "" } ], "url": "http://localhost:5000/administration/configuration", "folder": null, "queryParams": [], "preRequestScript": null, "pathVariables": {}, "pathVariableData": [], "method": "GET", "data": null, "dataMode": "params", "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "time": 1508849878025, "name": "GET http://localhost:5000/admin/configuration", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "responses": [], "isFromCollection": true, "collectionRequestId": "178f16da-c61b-c881-1c33-9d64a56851a4", "rawModeData": null, "descriptionFormat": null }, { "id": "37bfa9f1-fe29-6a68-e558-66d125d2c96f", "headers": "", "headerData": [], "url": "http://localhost:5000/administration/connect/token", "folder": null, "queryParams": [], "preRequestScript": null, "pathVariables": {}, "pathVariableData": [], "method": "POST", "data": [ { "key": "client_id", "value": "admin", "type": "text", "enabled": true }, { "key": "client_secret", "value": "secret", "type": "text", "enabled": true }, { "key": "scope", "value": "admin", "type": "text", "enabled": true }, { "key": "username", "value": "admin", "type": "text", "enabled": true }, { "key": "password", "value": "secret", "type": "text", "enabled": true }, { "key": "grant_type", "value": "password", "type": "text", "enabled": true } ], "dataMode": "params", "tests": "var jsonData = JSON.parse(responseBody);\npostman.setGlobalVariable(\"AccessToken\", jsonData.access_token);\npostman.setGlobalVariable(\"RefreshToken\", jsonData.refresh_token);", "currentHelper": "normal", "helperAttributes": "{}", "time": 1506359585080, "name": "POST http://localhost:5000/admin/connect/token copy", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "responses": [], "isFromCollection": true, "collectionRequestId": "37bfa9f1-fe29-6a68-e558-66d125d2c96f", "rawModeData": null, "descriptionFormat": null }, { "folder": null, "id": "4684c2fa-f38c-c193-5f55-bf563a1978c6", "name": "DELETE http://localhost:5000/posts/1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "DELETE", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "id": "5f308240-79e3-cf74-7a6b-fe462f0d54f1", "headers": "Authorization: Bearer {{AccessToken}}\n", "headerData": [ { "key": "Authorization", "value": "Bearer {{AccessToken}}", "description": "", "enabled": true } ], "url": "http://localhost:5000/administration/.well-known/openid-configuration", "folder": null, "queryParams": [], "preRequestScript": null, "pathVariables": {}, "pathVariableData": [], "method": "GET", "data": null, "dataMode": "params", "tests": null, "currentHelper": "normal", "helperAttributes": {}, "time": 1508849923518, "name": "GET http://localhost:5000/admin/.well-known/openid-configuration", "description": "", "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375", "responses": [] }, { "folder": null, "id": "a1c95935-ed18-d5dc-bcb8-a3db8ba1934f", "name": "GET http://localhost:5000/posts", "dataMode": "params", "data": [ { "key": "client_id", "value": "admin", "type": "text", "enabled": true }, { "key": "client_secret", "value": "secret", "type": "text", "enabled": true }, { "key": "scope", "value": "admin", "type": "text", "enabled": true }, { "key": "username", "value": "admin", "type": "text", "enabled": true }, { "key": "password", "value": "admin", "type": "text", "enabled": true }, { "key": "grant_type", "value": "password", "type": "text", "enabled": true } ], "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "POST", "pathVariables": {}, "url": "http://localhost:5000/admin/configuration", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "c4494401-3985-a5bf-71fb-6e4171384ac6", "name": "GET http://localhost:5000/posts/1/comments", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/posts/1/comments", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "c45d30d7-d9c4-fa05-8110-d6e769bb6ff9", "name": "PATCH http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "rawModeData": "{\n \"title\": \"gfdgsgsdgsdfgsdfgdfg\",\n}", "descriptionFormat": "html", "description": "", "headers": "", "method": "PATCH", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "e8825dc3-4137-99a7-0000-ef5786610dc3", "name": "POST http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}", "descriptionFormat": "html", "description": "", "headers": "", "method": "POST", "pathVariables": {}, "url": "http://localhost:5000/posts", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "ea0ed57a-2cb9-8acc-47dd-006b8db2f1b2", "name": "GET http://localhost:5000/posts/1", "dataMode": "params", "data": null, "rawModeData": null, "descriptionFormat": "html", "description": "", "headers": "", "method": "GET", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" }, { "folder": null, "id": "fddfc4fa-5114-69e3-4744-203ed71a526b", "name": "PUT http://localhost:5000/posts/1", "dataMode": "raw", "data": [], "rawModeData": "{\n \"userId\": 1,\n \"title\": \"test\",\n \"body\": \"test\"\n}", "descriptionFormat": "html", "description": "", "headers": "", "method": "PUT", "pathVariables": {}, "url": "http://localhost:5000/posts/1", "preRequestScript": null, "tests": null, "currentHelper": "normal", "helperAttributes": "{}", "queryParams": [], "headerData": [], "pathVariableData": [], "responses": [], "collectionId": "4dbde9fe-89f5-be35-bb9f-d3b438e16375" } ] } ================================================ FILE: test/Ocelot.ManualTest/Program.cs ================================================ using Ocelot.ManualTest.Actions; using System.Reflection; var nl = Environment.NewLine; var programName = Assembly.GetExecutingAssembly().GetName()?.Name?.Replace(".", " ") ?? "?"; do { Console.Clear(); Console.WriteLine($"{nl}Welcome to {programName} app!"); Console.Write(@"What are you going to do? 1. Run Ocelot with basic setup (default) 2. Run Ocelot manual tests So, press 1 or 2 > "); ConsoleKeyInfo info = Console.ReadKey(true); if (info.Key == ConsoleKey.D2) { Console.WriteLine((char)info.Key); ManualTests.Run(args); } else { Console.WriteLine($"{(char)info.Key} -> 1 (default)"); await Basic.RunAsync(args); } } while (!Quit()); bool Quit() { Console.WriteLine(nl + "Enter Ctrl+Q to Quit, Ctrl+E to Exit, Ctrl+L to Clear the log"); Console.Write("Or press any key to restart... "); ConsoleKeyInfo info = Console.ReadKey(true); if (info.Modifiers == ConsoleModifiers.Control) { if (info.Key == ConsoleKey.Q) { Console.WriteLine("Quitting..."); Environment.ExitCode = 0; return true; } else if (info.Key == ConsoleKey.E) { Console.WriteLine("Exitting..."); Environment.Exit(1); } else if (info.Key == ConsoleKey.L) { Console.WriteLine(); Console.Clear(); } } Console.WriteLine(); return false; } ================================================ FILE: test/Ocelot.ManualTest/Properties/launchSettings.json ================================================ { "iisSettings": { "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { "applicationUrl": "http://localhost:24620/", "sslPort": 0 } }, "profiles": { "IIS Express": { "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Ocelot.ManualTest": { "commandName": "Project", "launchUrl": "http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ================================================ FILE: test/Ocelot.ManualTest/Tests/Bug0930.html ================================================  Bug 930 Test

Bug 930 Test


WebSocket Connection





================================================ FILE: test/Ocelot.ManualTest/Usings.cs ================================================ // Default Microsoft.NET.Sdk.Web namespaces global using System.Net.Http.Json; global using Microsoft.AspNetCore.Builder; global using Microsoft.AspNetCore.Hosting; global using Microsoft.AspNetCore.Http; global using Microsoft.AspNetCore.Routing; global using Microsoft.Extensions.Configuration; global using Microsoft.Extensions.DependencyInjection; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; // Project extra global namespaces global using Ocelot; global using Ocelot.Testing; global using System; ================================================ FILE: test/Ocelot.ManualTest/appsettings.Development.json ================================================ { "Logging": { "LogLevel": { "Default": "Debug", "Microsoft": "Information", "Microsoft.AspNetCore": "Information", "Microsoft.Hosting.Lifetime": "Information", "System": "Information" } } } ================================================ FILE: test/Ocelot.ManualTest/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*" } ================================================ FILE: test/Ocelot.ManualTest/docker-compose.yaml ================================================ version: "3.4" services: tests: build: context: . target: builder volumes: - type: bind source: . target: /results command: test --logger:trx -r /results benchmarks: build: context: . target: builder args: build_configuration: Release command: run -c Release --project test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj 0 1 2 3 4 manual-test: build: . ports: [ "5000:80" ] ================================================ FILE: test/Ocelot.ManualTest/ocelot.identityserver4.json ================================================ { "Routes": [ { "DownstreamPathTemplate": "/", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 52876 } ], "UpstreamPathTemplate": "/identityserverexample", "UpstreamHttpMethod": [ "Get" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "AuthenticationOptions": { "AuthenticationProviderKey": "TestKey", "AllowedScopes": [ "openid", "offline_access" ] }, "AddHeadersToRequest": { "CustomerId": "Claims[CustomerId] > value", "LocationId": "Claims[LocationId] > value", "UserType": "Claims[sub] > value[0] > |", "UserId": "Claims[sub] > value[1] > |" }, "AddClaimsToRequest": { "CustomerId": "Claims[CustomerId] > value", "LocationId": "Claims[LocationId] > value", "UserType": "Claims[sub] > value[0] > |", "UserId": "Claims[sub] > value[1] > |" }, "AddQueriesToRequest": { "CustomerId": "Claims[CustomerId] > value", "LocationId": "Claims[LocationId] > value", "UserType": "Claims[sub] > value[0] > |", "UserId": "Claims[sub] > value[1] > |" }, "RouteClaimsRequirement": { "UserType": "registered" }, "RequestIdKey": "OcRequestId" }, { "DownstreamPathTemplate": "/posts", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ], "UpstreamPathTemplate": "/posts", "UpstreamHttpMethod": [ "Get" ], "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts/{postId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Get" ], "RequestIdKey": "RouteRequestId", "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, "UseTracing": true, "UseProxy": true }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts/{postId}/comments", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}/comments", "UpstreamHttpMethod": [ "Get" ], "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, "UseTracing": false }, "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/comments", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/comments", "UpstreamHttpMethod": [ "Get" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts", "UpstreamHttpMethod": [ "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts/{postId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Put" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts/{postId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Patch" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/posts/{postId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Delete" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/api/products", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/products", "UpstreamHttpMethod": [ "Get" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "FileCacheOptions": { "TtlSeconds": 15 } }, { "DownstreamPathTemplate": "/api/products/{productId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/products/{productId}", "UpstreamHttpMethod": [ "Get" ], "FileCacheOptions": { "TtlSeconds": 15 } }, { "DownstreamPathTemplate": "/api/products", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/products", "UpstreamHttpMethod": [ "Post" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 } }, { "DownstreamPathTemplate": "/api/products/{productId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/products/{productId}", "UpstreamHttpMethod": [ "Put" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "FileCacheOptions": { "TtlSeconds": 15 } }, { "DownstreamPathTemplate": "/posts", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/", "UpstreamHttpMethod": [ "Get" ], "QoSOptions": { "ExceptionsAllowedBeforeBreaking": 3, "DurationOfBreak": 10, "TimeoutValue": 5000 }, "FileCacheOptions": { "TtlSeconds": 15 } }, { "DownstreamPathTemplate": "/posts", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ], "UpstreamPathTemplate": "/list-post", "UpstreamHttpMethod": [ "GET" ], "Metadata": { "api_id": "e99d7ce0-d918-443e-b243-1960a8212b5d" } } ], "GlobalConfiguration": { "RequestIdKey": "ot-traceid" } } ================================================ FILE: test/Ocelot.ManualTest/ocelot.json ================================================ { "Routes": [ { "DownstreamPathTemplate": "/posts/{postId}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}", "UpstreamHttpMethod": [ "Get", "Put", "Patch", "Delete" ], "RequestIdKey": "RouteRequestId", "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, "UseTracing": true, "UseProxy": true } }, { "DownstreamPathTemplate": "/posts/{postId}/comments", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/posts/{postId}/comments", "UpstreamHttpMethod": [ "Get" ], "HttpHandlerOptions": { "AllowAutoRedirect": true, "UseCookieContainer": true, "UseTracing": false } }, { "DownstreamPathTemplate": "/comments/{id}", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 80 } ], "UpstreamPathTemplate": "/comments/{id}", "UpstreamHttpMethod": [ "Get" ], "Key": "C1", "RateLimitOptions": { "EnableRateLimiting": true, "Limit": 3, "Period": "1m", // 1 minute "PeriodTimespan": 3 // 3 seconds } }, { "DownstreamPathTemplate": "/posts", "DownstreamScheme": "https", "DownstreamHostAndPorts": [ { "Host": "jsonplaceholder.typicode.com", "Port": 443 } ], "UpstreamPathTemplate": "/metadata/posts", "UpstreamHttpMethod": [ "GET" ], "Metadata": { "api_id": "e99d7ce0-d918-443e-b243-1960a8212b5d" } }, { "UpstreamPathTemplate": "/bug930/ws1", "DownstreamPathTemplate": "/WebSocket/EchoWebSocket.ashx", "DownstreamScheme": "ws", "DownstreamHostAndPorts": [ { "Host": "corefx-net-http11.azurewebsites.net", "Port": 80 } ], "Metadata": { "bug_930": "https://github.com/ThreeMammals/Ocelot/issues/930", "PR_2091": "https://github.com/ThreeMammals/Ocelot/pull/2091" } }, { "UpstreamPathTemplate": "/bug930/ws2", "DownstreamPathTemplate": "/", "DownstreamScheme": "wss", "DownstreamHostAndPorts": [ { "Host": "echo.websocket.org", "Port": 443 } ], "Metadata": { "bug_930": "https://github.com/ThreeMammals/Ocelot/issues/930", "PR_2091": "https://github.com/ThreeMammals/Ocelot/pull/2091" } }, { "UpstreamPathTemplate": "/bug930/ws3", "DownstreamPathTemplate": "/raw", "DownstreamScheme": "wss", "DownstreamHostAndPorts": [ { "Host": "ws.postman-echo.com", "Port": 443 } ], "Metadata": { "bug_930": "https://github.com/ThreeMammals/Ocelot/issues/930", "PR_2091": "https://github.com/ThreeMammals/Ocelot/pull/2091" } } ], "GlobalConfiguration": { "RequestIdKey": "ot-traceid", "RateLimitOptions": { "ClientIdHeader": "MyRateLimiting", "QuotaExceededMessage": "Customize Tips!", "RateLimitCounterPrefix": "ocelot", "Keys": ["C1"] }, "RateLimitingRules": [ { "Name": "MetadataRule", "Pattern": "/metadata/posts", "Methods": [ "GET" ], "Limit": 1, "Period": "1m", "DisableRateLimitHeaders": false, "QuotaExceededMessage": "You have exceeded the rate limit quota for grouped route by '/metadata/posts' pattern! Please try again later." }, { "Name": "PostsRule", "Pattern": "/posts/*", "Methods": [ "GET" ], "Limit": 3, "Period": "1m", "DisableRateLimitHeaders": true, "QuotaExceededMessage": "You have exceeded the rate limit quota for grouped routes by '/posts/*' pattern! Please try again later." } ] } } ================================================ FILE: test/Ocelot.ManualTest/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==" }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==" }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==" }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==" }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==" }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==" }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==" }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.0.1" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==", "dependencies": { "Microsoft.IdentityModel.Logging": "8.0.1" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.AspNetCore.TestHost": "[10.0.5, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )" } } }, "net10.0/osx-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net10.0/win-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net8.0": { "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "33eTIA2uO/L9utJjZWbKsMSVsQf7F8vtd6q5mQX7ZJzNvCpci5fleD6AeANGlbbb7WX7XKxq9+Dkb5e3GNDrmQ==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "cloLGeZolXbCJhJBc5OC05uhrdhdPL6MWHuVUnkkUvPDeK7HkwThBaLZ1XjBQVk9YhxXE2OvHXnKi0PLleXxDg==", "dependencies": { "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "YCxBt2EeJP8fcXk9desChkWI+0vFqFLvBwrz5hBMsoh0KJE6BC66DnzkdzkJNqMltLromc52dkdT206jJ38cTw==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "7.1.2" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==", "dependencies": { "Microsoft.IdentityModel.Protocols": "7.1.2", "System.IdentityModel.Tokens.Jwt": "7.1.2" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "oICJMqr3aNEDZOwnH5SK49bR6Z4aX0zEAnOLuhloumOSuqnNq+GWBdQyrgILnlcT5xj09xKCP/7Y7gJYB+ls/g==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "Thhbe1peAmtSBFaV/ohtykXiZSOkx59Da44hvtWfIMFofDA3M3LaVyjstACf2rKGn4dEDR2cUpRAZ0Xs/zB+7Q==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.AspNetCore.TestHost": "[8.0.25, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net8.0/osx-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net8.0/win-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0": { "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==" }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.TestHost": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "OtlIWcyX01olfdevPKZdIPfBEvbcioDyBiE/Z2lHsopsMD7twcKtlN9kMevHmI5IIPhFpfwCIiR6qHQz1WHUIw==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "s6++gF9x0rQApQzOBbSyp4jUaAlwm+DroKfL8gdOHxs83k8SJfUXhuc46rDB3rNXBQ1MVRxqKUrqFhO/M0E97g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "UCPF2exZqBXe7v/6sGNiM6zCQOUXXQ9+v5VTb9gPB8ZSUPnX53BxlN78v2jsbIvK9Dq4GovQxo23x8JgWvm/Qg==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.0.1" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "kDimB6Dkd3nkW2oZPDkMkVHfQt3IDqO5gL0oa8WVy3OP4uE8Ij+8TXnqg9TOd9ufjsY3IDiGz7pCUbnfL18tjg==", "dependencies": { "Microsoft.IdentityModel.Logging": "8.0.1" } }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "GJw3bYkWpOgvN3tJo5X4lYUeIFA2HD293FPUhKmp7qxS+g5ywAb34Dnd3cDAFLkcMohy5XTpoaZ4uAHuw0uSPQ==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.0.1", "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.AspNetCore.TestHost": "[9.0.14, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net9.0/osx-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0/win-x64": { "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } } } } ================================================ FILE: test/Ocelot.ManualTest/tempkey.rsa ================================================ {"KeyId":"6aad821b3e366a0189ea6c9741eae6ba","Parameters":{"D":"RcfIF/iJ8qnKpHlaJCa9Qz+iN9Z655mfW0B/CycZx0WDQwQtjYNF+ijEkqfpGC3TJ9n19vXHdDEGfONxHwTtgS6PP/VIYYmql7OfCn+tkZUvMeIXykfEXFoNoWJXlT3eMI1JWyZpT/dZJLtmdeY09csDU/LjXTlyFrljW361T0NR/azAHqEfeuoKhqaJ2klzTzjif4xO5kMcTBHVyxrZm0+cbowsKPjI1QRh5xXsst8EDrM7rXStz4enneNaNbvP1nmWx++F7zn+5/WBDcPJVnL1HiyAzMAHj+oXG2JwDizO4RJxLbvQa2Y2jzoDp/qc++s2HWFo1PmuUnOzNIQjyQ==","DP":"x3VwsILF1yo8puLB6TOcb4hMWngz8rqjBl3dty4E3Un7UexVh+NkqTiSXWZNern6Ka6gE8CpdDXhQiYCFbcnBiSF6FVSpjpQ+Qf9PeRj5HipJF+DzGyEOTzwOiBjb6s5CvPUQWvJiqQoP1D+1V+X/+C0zug8+Df73UyHA7uieKc=","DQ":"aaZu9GfgKqUiy0uwPkmnLcwIEDqRlLG14c23OOoEqNRHK9T3OwUKEJ1qK0mbMKIVTwklyiC7uZHqMijIqk0LovTL4nI3LRlDkRjhUsIuubZZOAw7sYYemBUl+wEB/VZofaJ7H/CYtCUXyJhND2DFbTjzgeg3uWoMpyMDHuH/9Pc=","Exponent":"AQAB","InverseQ":"YRV4QA6rwB9BEHjzh5Wk3TcSS1CrwJWVopj/qC1amXxhtM3aMb4ZfKk7XoynLqHyQ86rB2p7dPNP2GL+fIWbt4h/ESO9JqOlM/bVXpdyCvIwchAL82hfHb4FRv+V+J2cksaIA+bHt7ye9n/XSmSr8v0WsjxN2qHzdla3t+J0c1I=","Modulus":"xLJZzQyYbJAqymvvJeig9H4cfXEy3t0KVnRuUumdSBzU/3F7q1vCDBkXLqs8icEv9ZL4MUgDzzSjjYJpVXzvC24L1My3NLhSSZOCGrhSHCx98+cAgrb71tirXXsiBMXKeGhnJ05KopHtPRJVxBvd3d3Kee957y86g1Sbho1XxwWsrzVu5E7YZS+NkJycHkiUseMKeQ+tMLbPoFZPu5EqrrsSWuDjb7XNUjJViyGaOtvL2SQ9QtvDu006fe4m0VVw71ycSt2ReAmlA+EgCFsyLYBIoAhlk7k+lKyYO3a/8E0bzltB6MGaRaGJMB0C9pSxAfGSmSpIi2OUy0YIpIoDoQ==","P":"ydx6DVoXf3DgS6WZtrR82xNf12kLD5cGUToPwwIjjX5oQywOhGXOV4GCrqISDff2bosrPvleBfuJ5KH9KRVAaEjh1At554Bq+Nw8cc/1mTXEOSKENDtA9GjkpthR0QW1FDFRR5Tc8sRuoBpulN1rJIDIkfEkqwlpugFmk2UrDk8=","Q":"+XNIV8qMorQ11C1fVj4L91wufF4NqVqCdm/PN3f+xZ5UWoiCOil+njRuIL09ZifEwy3fgqD06Fu/SvaqMODyKAzA+RMUJU0sk92aOzAhKiGBk38sEvEuDUKZYNJm5NLjo9XXBG8DQzSUPvmIFLaMCloA95Ozie0mJcrXcimCww8="}} ================================================ FILE: test/Ocelot.UnitTests/Authentication/AuthenticationMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Middleware; using System.Security.Claims; using System.Security.Principal; using System.Text; using AuthenticationMiddleware = Ocelot.Authentication.AuthenticationMiddleware; using AuthenticationOptions = Ocelot.Configuration.AuthenticationOptions; namespace Ocelot.UnitTests.Authentication; public class AuthenticationMiddlewareTests : UnitTest { private readonly Mock _authentication; private readonly Mock _factory; private readonly Mock _logger; private readonly Mock _serviceProvider; private readonly DefaultHttpContext _httpContext; private AuthenticationMiddleware _middleware; private RequestDelegate _next; private bool _isNextCalled; public AuthenticationMiddlewareTests() { _authentication = new Mock(); _serviceProvider = new Mock(); _serviceProvider.Setup(sp => sp.GetService(typeof(IAuthenticationService))).Returns(_authentication.Object); _httpContext = new DefaultHttpContext { RequestServices = _serviceProvider.Object, }; _factory = new Mock(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _logger.Setup(x => x.LogInformation(It.IsAny>())) .Callback>(f => _logInformationMessages.Add(f.Invoke())); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(f => _logWarningMessages.Add(f.Invoke())); } [Fact] public void MiddlewareName_Cstor_ReturnsTypeName() { // Arrange _isNextCalled = false; _next = (context) => { _isNextCalled = true; return Task.CompletedTask; }; _middleware = new AuthenticationMiddleware(_next, _factory.Object); var expected = _middleware.GetType().Name; // Act var actual = _middleware.MiddlewareName; // Assert Assert.False(_isNextCalled); Assert.NotNull(actual); Assert.Equal(expected, actual); } [Fact] public async Task Should_call_next_middleware_if_route_is_not_authenticated() { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamHttpMethod([HttpMethods.Get]) .WithAuthenticationOptions(new()) .Build(); GivenTheDownStreamRouteIs(route); // Act await WhenICallTheMiddleware(route.IsAuthenticated); // Assert ThenTheUserIsAuthenticated("The user is NOT authenticated"); } [Fact] public async Task Should_call_next_middleware_if_route_is_using_options_method() { // Arrange GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() .WithUpstreamHttpMethod([HttpMethods.Options]) .WithAuthenticationOptions(new()) .Build()); GivenTheRequestIsUsingMethod(HttpMethods.Options); // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsAuthenticated(); } [Fact] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task Should_call_next_middleware_if_route_is_using_several_options_authentication_providers() { // Arrange var multipleKeys = new string[] { string.Empty, "Fail", "Test" }; var options = new AuthenticationOptions(null, multipleKeys); var methods = new List { HttpMethods.Get }; GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() .WithAuthenticationOptions(options) .WithUpstreamHttpMethod(methods) .Build()); GivenTheRequestIsUsingMethod(methods.First()); GivenTheAuthenticationIsFail(); GivenTheAuthenticationIsSuccess(); GivenTheAuthenticationThrowsException(); // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsAuthenticated(); } [Fact] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task Should_provide_backward_compatibility_if_route_has_several_options_authentication_providers() { // Arrange FileAuthenticationOptions opts = new() { AuthenticationProviderKey = "Test", AuthenticationProviderKeys = new[] { string.Empty, "Fail", "Test" }, }; AuthenticationOptions options = new(opts); var methods = new List { HttpMethods.Get }; GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() .WithAuthenticationOptions(options) .WithUpstreamHttpMethod(methods) .Build()); GivenTheRequestIsUsingMethod(methods.First()); GivenTheAuthenticationIsFail(); GivenTheAuthenticationIsSuccess(); GivenTheAuthenticationThrowsException(); // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsAuthenticated(); } [Fact] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task Should_not_call_next_middleware_and_return_no_result_if_all_multiple_keys_were_failed() { // Arrange var options = new AuthenticationOptions(null, new[] { string.Empty, "Fail", "Fail", "UnknownScheme" }); var methods = new List { HttpMethods.Get }; GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() .WithAuthenticationOptions(options) .WithUpstreamHttpMethod(methods) .Build()); GivenTheRequestIsUsingMethod(methods.First()); GivenTheAuthenticationIsFail(); GivenTheAuthenticationIsSuccess(); // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsNotAuthenticated(); _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse(); _logWarningMessages.Count.ShouldBe(1); _logWarningMessages.First().ShouldStartWith("Client has NOT been authenticated for path"); _httpContext.Items.Errors().First().ShouldBeOfType(); } private void GivenHappyPath(bool isHappy = true, string userName = null) { var multipleKeys = new string[] { "Test" }; var options = new AuthenticationOptions(null, multipleKeys); string[] methods = [HttpMethods.Get]; GivenTheDownStreamRouteIs(new DownstreamRouteBuilder() .WithAuthenticationOptions(options) .WithUpstreamHttpMethod(methods) .Build()); GivenTheRequestIsUsingMethod(methods[0]); GivenTheAuthenticationIsFail(); GivenTheAuthenticationIsSuccess(isHappy, userName); // Identity.IsAuthenticated -> true by default GivenTheAuthenticationThrowsException(); } [Fact] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task Should_SetUnauthenticatedError_and_not_call_next_middleware_if_identity_IsNOT_authenticated() { // Arrange GivenHappyPath(false);// Identity.IsAuthenticated -> false // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsNotAuthenticated(); Assert.False(_isNextCalled); _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse(); _logInformationMessages.Count.ShouldBe(1); _logWarningMessages.Count.ShouldBe(1); _logWarningMessages[0].ShouldBe("Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;"); _httpContext.Items.Errors().Count.ShouldBe(1); var e = _httpContext.Items.Errors()[0]; Assert.IsType(e); Assert.Equal("Request for authenticated route '' was unauthenticated!", e.Message); } [Theory] [InlineData("", 0)] [InlineData("Igor", 1)] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task SetUnauthenticatedError(string userName, int index) { // Arrange var warnings = new string[] { "Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;", "Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' by 'Igor' was unauthenticated!;", }; var messages = new string[] { "Request for authenticated route '' was unauthenticated!", "Request for authenticated route '' by 'Igor' was unauthenticated!", }; GivenHappyPath(false, userName);// Identity.IsAuthenticated -> false // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsNotAuthenticated(); Assert.False(_isNextCalled); Assert.Equal(warnings[index], _logWarningMessages[0]); var e = _httpContext.Items.Errors()[0]; Assert.IsType(e); Assert.Equal(messages[index], e.Message); } [Theory] [InlineData(0)] [InlineData(2)] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task Should_not_call_next_middleware_and_return_no_result_if_providers_keys_are_empty(int keysCount) { // Arrange var emptyKeys = new string[keysCount]; for (int i = 0; i < emptyKeys.Length; i++) { emptyKeys[i] = i % 2 == 0 ? null : string.Empty; } var optionsWithEmptyKeys = new AuthenticationOptions(null, emptyKeys); var methods = new List { "Get" }; var route = new DownstreamRouteBuilder() .WithAuthenticationOptions(optionsWithEmptyKeys) .WithUpstreamHttpMethod(methods) .WithDownstreamPathTemplate("/" + TestName()) .Build(); GivenTheDownStreamRouteIs(route); GivenTheRequestIsUsingMethod(methods.First()); // Act await WhenICallTheMiddleware(route.IsAuthenticated); // Assert ThenTheUserIsAuthenticated("The user is NOT authenticated"); _httpContext.User.Identity.IsAuthenticated.ShouldBeFalse(); _logWarningMessages.Count.ShouldBe(0); _logInformationMessages.Count.ShouldBe(1); _logInformationMessages[0].ShouldBe("No authentication is required for the path '' in the route /Should_not_call_next_middleware_and_return_no_result_if_providers_keys_are_empty."); _httpContext.Items.Errors().Count(e => e.GetType() == typeof(UnauthenticatedError)).ShouldBe(0); } [Fact] [Trait("Feat", "740")] // https://github.com/ThreeMammals/Ocelot/issues/740 [Trait("Feat", "1580")] // https://github.com/ThreeMammals/Ocelot/issues/1580 [Trait("PR", "1870")] // https://github.com/ThreeMammals/Ocelot/pull/1870 public async Task AuthenticateAsync_CatchException() { // Arrange GivenHappyPath(false);// Identity.IsAuthenticated -> false _authentication .Setup(a => a.AuthenticateAsync(It.IsAny(), It.IsAny())) .Throws((ctx, scheme) => new("Bad auth scheme -> " + scheme)); // Act await WhenICallTheMiddleware(); // Assert ThenTheUserIsNotAuthenticated(); Assert.False(_isNextCalled); Assert.False(_httpContext.User.Identity.IsAuthenticated); Assert.Equal(2, _logWarningMessages.Count); Assert.Equal("Unable to authenticate the client for route '?' using the Test authentication scheme due to error: Bad auth scheme -> Test", _logWarningMessages[0]); Assert.Equal("Client has NOT been authenticated for path '' and pipeline error set. UnauthenticatedError: Request for authenticated route '' was unauthenticated!;", _logWarningMessages[1]); var errors = _httpContext.Items.Errors(); Assert.Equal(1, errors.Count); Assert.IsType(errors[0]); Assert.Equal("Request for authenticated route '' was unauthenticated!", errors[0].Message); } private readonly List _logInformationMessages = new(); private readonly List _logWarningMessages = new(); private void GivenTheAuthenticationIsFail() { _authentication .Setup(a => a.AuthenticateAsync(It.IsAny(), It.Is(s => s.Equals("Fail")))) .Returns(Task.FromResult(AuthenticateResult.Fail("The user is not authenticated."))); } private void GivenTheAuthenticationIsSuccess(bool isAuthenticated = true, string userName = null) { var principal = new Mock(); var identity = new Mock(); identity.Setup(i => i.IsAuthenticated).Returns(isAuthenticated); identity.Setup(i => i.Name).Returns(userName ?? string.Empty); principal.Setup(p => p.Identity).Returns(identity.Object); _authentication .Setup(a => a.AuthenticateAsync(It.IsAny(), It.Is(s => s.Equals("Test")))) .Returns(Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal.Object, "Test")))); } private void GivenTheAuthenticationThrowsException() { _authentication .Setup(a => a.AuthenticateAsync(It.IsAny(), It.Is(scheme => string.Empty.Equals(scheme)))) .Throws(new InvalidOperationException("Authentication provider key is empty.")); } private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute) { _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); } private void GivenTheRequestIsUsingMethod(string method) { _httpContext.Request.Method = method; } private void ThenTheUserIsAuthenticated(string expected = null) { var content = _httpContext.Response.Body.AsString(); content.ShouldBe(expected ?? "The user is authenticated"); } private void ThenTheUserIsNotAuthenticated(string expected = null) { var content = _httpContext.Response.Body.AsString(); var errors = _httpContext.Items.Errors(); content.ShouldBe(expected ?? string.Empty); errors.ShouldNotBeEmpty(); } private Task WhenICallTheMiddleware(bool isAuthenticated = true) { _isNextCalled = false; _next = (context) => { _isNextCalled = true; var not = !isAuthenticated ? " NOT" : string.Empty; byte[] byteArray = Encoding.ASCII.GetBytes($"The user is{not} authenticated"); var stream = new MemoryStream(byteArray); _httpContext.Response.Body = stream; return Task.CompletedTask; }; _middleware = new AuthenticationMiddleware(_next, _factory.Object); return _middleware.Invoke(_httpContext); } } ================================================ FILE: test/Ocelot.UnitTests/Authentication/AuthenticationOptionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Reflection; namespace Ocelot.UnitTests.Authentication; public class AuthenticationOptionsCreatorTests { private readonly AuthenticationOptionsCreator _creator; private readonly List _routeScopes = new() { "route scope 1", "route scope 2" }; private const string _routeAuthProviderKey = "route key"; private readonly string[] _routeAuthProviderKeys = new string[] { "route key 1", "route key 2" }; private readonly List _globalScopes = new() { "global scope 1", "global scope 2" }; private const string _globalAuthProviderKey = "global key"; private readonly string[] _globalAuthProviderKeys = new string[] { "global key 1", "global key 2" }; public AuthenticationOptionsCreatorTests() { _creator = new AuthenticationOptionsCreator(); } [Theory] [InlineData(false)] [InlineData(true)] public void Create_OptionsObjIsNotNull_CreatedFromRoute(bool isAuthenticationProviderKeys) { string authenticationProviderKey = !isAuthenticationProviderKeys ? _routeAuthProviderKey : null; string[] authenticationProviderKeys = isAuthenticationProviderKeys ? _routeAuthProviderKeys : null; var route = CreateFileRoute(_routeScopes, authenticationProviderKey, authenticationProviderKeys); // Act var actual = _creator.Create(route, new()); // Assert actual.AllowedScopes.ShouldBe(_routeScopes); var expectedKeys = Enumerable .Repeat(authenticationProviderKey, !isAuthenticationProviderKeys ? 1 : 0) .Concat(authenticationProviderKeys ?? Enumerable.Empty()) .ToArray(); actual.AuthenticationProviderKeys.ShouldBe(expectedKeys); } #region PR 2114 [Theory] [InlineData(false)] [InlineData(true)] [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 public void Create_GlobalAuthOptsObjIsNull_CreatedSuccessfully(bool isNull) { // Arrange FileRoute arg1 = new(); FileGlobalConfiguration arg2 = new() { AuthenticationOptions = isNull ? new() : null, }; // Act var actual = _creator.Create(arg1, arg2); // Assert Assert.NotNull(actual); } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public void Create_GlobalConfigExists_ShouldUseGlobal() { // Arrange var route = new FileRoute(); var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys); var expected = new AuthenticationOptions(globalConfig.AuthenticationOptions); // Act var actual = _creator.Create(route, globalConfig); // Assert actual.AllowedScopes.ShouldBe(expected.AllowedScopes); actual.AuthenticationProviderKeys.ShouldBe(expected.AuthenticationProviderKeys); } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public void Create_RouteKeyProviderEmpty_ShouldUseGlobal() { // Arrange var route = CreateFileRoute(_routeScopes, string.Empty, null); var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys); // Act var actual = _creator.Create(route, globalConfig); // Assert actual.AllowedScopes.ShouldBe(_routeScopes); actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKey); actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKeys[0]); actual.AuthenticationProviderKeys.ShouldContain(_globalAuthProviderKeys[1]); } [Theory] [InlineData(false)] [InlineData(true)] [Trait("PR", "2114")] [Trait("Feat", "842")] public void Create_RouteAndGlobalKeyExist_ShouldUseRoute(bool routeHasSingleProviderKey) { // Arrange var routeAuthProviderKey = routeHasSingleProviderKey ? _routeAuthProviderKey : null; var routeAuthProviderKeys = routeHasSingleProviderKey ? null : _routeAuthProviderKeys; var route = CreateFileRoute(_routeScopes, routeAuthProviderKey, routeAuthProviderKeys); var globalConfig = CreateGlobalConfiguration(_globalScopes, _globalAuthProviderKey, _globalAuthProviderKeys); // Act var actual = _creator.Create(route, globalConfig); // Assert actual.AllowedScopes.ShouldBe(_routeScopes); if (routeHasSingleProviderKey) { actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKey); actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKeys[0]); actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKeys[1]); } else { actual.AuthenticationProviderKeys.ShouldNotContain(_routeAuthProviderKey); actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKeys[0]); actual.AuthenticationProviderKeys.ShouldContain(_routeAuthProviderKeys[1]); } } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public void Create() { // Arrange FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; var ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), ex.ParamName); route = new(); ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), ex.ParamName); globalConfiguration = new(); var actual = _creator.Create(route, globalConfiguration); Assert.NotNull(actual); route.AuthenticationOptions = globalConfiguration.AuthenticationOptions = null; actual = _creator.Create(route, globalConfiguration); Assert.NotNull(actual); route.AuthenticationOptions = new(); globalConfiguration.AuthenticationOptions = new() { AllowedScopes = ["test"], AuthenticationProviderKeys = ["test"], }; actual = _creator.Create(route, globalConfiguration); Assert.NotNull(actual); route.AuthenticationOptions.AuthenticationProviderKeys = ["test"]; route.AuthenticationOptions.AllowedScopes = ["test"]; globalConfiguration.AuthenticationOptions.AuthenticationProviderKeys = []; globalConfiguration.AuthenticationOptions.AllowedScopes = []; actual = _creator.Create(route, globalConfiguration); Assert.NotNull(actual); } #endregion PR 2114 #region PR 2336 [Fact] [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 public void Create_FileAuthenticationOptions() { // Arrange FileAuthenticationOptions options = new() { AllowAnonymous = true, AllowedScopes = new() { "scope" }, AuthenticationProviderKey = "key1", AuthenticationProviderKeys = new[] { "key2" }, }; // Act var actual = _creator.Create(options); // Assert Assert.NotNull(actual); Assert.True(actual.AllowAnonymous); Assert.Contains("scope", actual.AllowedScopes); Assert.Contains("key1", actual.AuthenticationProviderKeys); Assert.Contains("key2", actual.AuthenticationProviderKeys); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void Create_FileRoute_ArgumentNullChecks() { // Arrange FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; // Act, Assert var ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), ex.ParamName); // Act, Assert route = new(); ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), ex.ParamName); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void Create_FromRoute() { // Arrange FileRoute route = new() { AuthenticationOptions = new() { AllowAnonymous = false, AllowedScopes = null, AuthenticationProviderKey = "route", AuthenticationProviderKeys = null, }, }; FileGlobalConfiguration globalConfiguration = new() { AuthenticationOptions = new() { AllowAnonymous = null, AllowedScopes = ["global"], AuthenticationProviderKey = null, AuthenticationProviderKeys = ["global"], }, }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.False(actual.AllowAnonymous); Assert.Contains("global", actual.AllowedScopes); Assert.Contains("route", actual.AuthenticationProviderKeys); Assert.Contains("global", actual.AuthenticationProviderKeys); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void Create_FromDynamicRoute_NullChecks() { // Arrange, Act, Assert FileDynamicRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void Create_FromDynamicRoute() { // Arrange FileDynamicRoute route = new() { AuthenticationOptions = new() { AllowAnonymous = false, AllowedScopes = null, AuthenticationProviderKey = "route", AuthenticationProviderKeys = null, }, }; FileGlobalConfiguration globalConfiguration = new() { AuthenticationOptions = new() { AllowAnonymous = null, AllowedScopes = ["global"], AuthenticationProviderKey = null, AuthenticationProviderKeys = ["global"], }, }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.False(actual.AllowAnonymous); Assert.Contains("global", actual.AllowedScopes); Assert.Contains("route", actual.AuthenticationProviderKeys); Assert.Contains("global", actual.AuthenticationProviderKeys); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void CreateProtected_NullCheck() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); IRouteGrouping grouping = null; FileAuthenticationOptions options = null; FileGlobalAuthenticationOptions globalOptions = null; // Act var wrapper = Assert.Throws( () => method.Invoke(_creator, [grouping, options, globalOptions])); // Assert Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(grouping), actual.ParamName); } [Fact] [Trait("PR", "2336")] [Trait("Feat", "2316")] public void CreateProtected() { // Arrange FileAuthenticationOptions options = null; FileDynamicRoute route = new() { Key = "r1", AuthenticationOptions = options, }; FileGlobalAuthenticationOptions globalOptions = new() { RouteKeys = null, AllowAnonymous = null, AllowedScopes = ["global"], AuthenticationProviderKey = "globalKey", AuthenticationProviderKeys = ["global1", "global2"], }; FileGlobalConfiguration globalConfiguration = new() { AuthenticationOptions = globalOptions, }; // Act, Assert var actual = _creator.Create(route, globalConfiguration); Assert.False(actual.AllowAnonymous); Assert.Contains("global", actual.AllowedScopes); Assert.Contains("globalKey", actual.AuthenticationProviderKeys); Assert.Contains("global1", actual.AuthenticationProviderKeys); Assert.Contains("global2", actual.AuthenticationProviderKeys); // Arrange 2 route.AuthenticationOptions = options = new() { AllowAnonymous = true, AllowedScopes = ["route"], AuthenticationProviderKey = "route", AuthenticationProviderKeys = ["route1", "route2"], }; globalOptions.RouteKeys = ["?"]; // Act, Assert 2 actual = _creator.Create(route, globalConfiguration); Assert.True(actual.AllowAnonymous); Assert.Contains("route", actual.AllowedScopes); Assert.Contains("route", actual.AuthenticationProviderKeys); Assert.Contains("route1", actual.AuthenticationProviderKeys); Assert.Contains("route2", actual.AuthenticationProviderKeys); globalOptions.RouteKeys = ["r1"]; actual = _creator.Create(route, globalConfiguration); Assert.True(actual.AllowAnonymous); Assert.Contains("route", actual.AllowedScopes); Assert.Contains("route", actual.AuthenticationProviderKeys); Assert.Contains("route1", actual.AuthenticationProviderKeys); Assert.Contains("route2", actual.AuthenticationProviderKeys); globalConfiguration.AuthenticationOptions = globalOptions = null; actual = _creator.Create(route, globalConfiguration); Assert.True(actual.AllowAnonymous); Assert.Contains("route", actual.AllowedScopes); Assert.Contains("route", actual.AuthenticationProviderKeys); Assert.Contains("route1", actual.AuthenticationProviderKeys); Assert.Contains("route2", actual.AuthenticationProviderKeys); // Arrange 3 : Merging options.AllowAnonymous = null; options.AllowedScopes = null; options.AuthenticationProviderKey = null; globalConfiguration.AuthenticationOptions = globalOptions = new() { RouteKeys = null, AllowAnonymous = false, AllowedScopes = ["global"], AuthenticationProviderKey = "globalKey", AuthenticationProviderKeys = ["global1", "global2"], }; actual = _creator.Create(route, globalConfiguration); Assert.False(actual.AllowAnonymous); Assert.Contains("global", actual.AllowedScopes); Assert.Contains("globalKey", actual.AuthenticationProviderKeys); Assert.Contains("route1", actual.AuthenticationProviderKeys); Assert.Contains("route2", actual.AuthenticationProviderKeys); } #endregion PR 2336 private static FileRoute CreateFileRoute(List allowedScopes, string authProviderKey, string[] authProviderKeys) => new() { AuthenticationOptions = new() { AllowedScopes = allowedScopes, AuthenticationProviderKey = authProviderKey, AuthenticationProviderKeys = authProviderKeys, }, }; private static FileGlobalConfiguration CreateGlobalConfiguration(List allowedScopes, string authProviderKey, string[] authProviderKeys) => new() { AuthenticationOptions = new() { AllowedScopes = allowedScopes, AuthenticationProviderKey = authProviderKey, AuthenticationProviderKeys = authProviderKeys, }, }; } ================================================ FILE: test/Ocelot.UnitTests/Authentication/FileAuthenticationOptionsTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Authentication; public class FileAuthenticationOptionsTests { [Fact] public void ToString_Serialized() { // Arrange FileAuthenticationOptions opts = new() { AllowAnonymous = true, AllowedScopes = ["2"], AuthenticationProviderKey = "3", AuthenticationProviderKeys = ["4", "5"], }; // Act var actual = opts.ToString(); // Assert Assert.False(string.IsNullOrWhiteSpace(actual)); Assert.Equal("AllowAnonymous:True,AllowedScopes:['2'],AuthenticationProviderKey:'3',AuthenticationProviderKeys:['4','5']", actual); } } ================================================ FILE: test/Ocelot.UnitTests/Authentication/FileGlobalAuthenticationOptionsTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Authentication; [Trait("Feat", "585")] [Trait("Feat", "2316")] // https://github.com/ThreeMammals/Ocelot/issues/2316 [Trait("PR", "2336")] // https://github.com/ThreeMammals/Ocelot/pull/2336 public class FileGlobalAuthenticationOptionsTests { [Fact] public void Constructor_ShouldInitializeBaseAndRouteKeys() { // Act var options = new FileGlobalAuthenticationOptions(); // Assert Assert.NotNull(options); Assert.Null(options.RouteKeys); // not initialized by default } [Fact] public void Constructor_WithAuthScheme_ShouldSetAuthScheme() { // Arrange var authScheme = "TestScheme"; // Act var options = new FileGlobalAuthenticationOptions(authScheme); // Assert Assert.NotNull(options); Assert.Single(options.AuthenticationProviderKeys, authScheme); } [Fact] public void Constructor_WithFileAuthenticationOptions_ShouldCopyValues() { // Arrange var from = new FileAuthenticationOptions() { AllowAnonymous = true, AllowedScopes = ["scope"], AuthenticationProviderKey = "key", AuthenticationProviderKeys = ["key1", "key2"], }; // Act var options = new FileGlobalAuthenticationOptions(from); // Assert Assert.NotNull(options); Assert.Null(options.RouteKeys); FileAuthenticationOptions actual = (FileAuthenticationOptions)options; Assert.Equivalent(from, actual); Assert.Equal(from.AuthenticationProviderKey, options.AuthenticationProviderKey); Assert.Contains("key1", options.AuthenticationProviderKeys); Assert.Contains("key2", options.AuthenticationProviderKeys); } [Fact] public void RouteKeys_ShouldAllowSetAndGet() { // Arrange var options = new FileGlobalAuthenticationOptions(); var keys = new HashSet { "route1", "route2" }; // Act options.RouteKeys = keys; // Assert Assert.NotNull(options.RouteKeys); Assert.Contains("route1", options.RouteKeys); Assert.Contains("route2", options.RouteKeys); } [Fact] public void ShouldImplementIRouteGroup() { // Act var options = new FileGlobalAuthenticationOptions(); // Assert Assert.True(options is IRouteGroup); } } ================================================ FILE: test/Ocelot.UnitTests/Authorization/AuthorizationMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Authorization; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Authorization; public class AuthorizationMiddlewareTests : UnitTest { private readonly Mock _claimsAuthorizer; private readonly Mock _scopesAuthorizer; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly AuthorizationMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public AuthorizationMiddlewareTests() { _httpContext = new DefaultHttpContext(); _claimsAuthorizer = new Mock(); _scopesAuthorizer = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new AuthorizationMiddleware(_next, _claimsAuthorizer.Object, _scopesAuthorizer.Object, _loggerFactory.Object); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(_warnings.Add); } private readonly List> _warnings = new(); private List GetWarnings() => _warnings.Select(w => w()).ToList(); [Fact] [Trait("Feat", "100")] // https://github.com/ThreeMammals/Ocelot/issues/100 [Trait("PR", "104")] // https://github.com/ThreeMammals/Ocelot/pull/104 [Trait("Release", "1.4.5")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5 public async Task Should_call_scopes_authorizer_when_route_is_authenticated() { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) .WithUpstreamHttpMethod([HttpMethods.Get]) .WithAuthenticationOptions(new(new("authScheme"))) .Build(); GivenTheDownStreamRouteIs(new(), route); GivenScopesAuthorizerReturns(new OkResponse(true)); // Act await _middleware.Invoke(_httpContext); // Assert ThenScopesAuthorizerIsCalled(); } [Fact] [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public async Task Should_call_authorization_service() { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) .WithUpstreamHttpMethod([HttpMethods.Get]) /*.WithAuthenticationOptions(new(new("authScheme")))*/ .WithRouteClaimsRequirement(new() { { "k", "v" } }) .Build(); GivenTheDownStreamRouteIs(new(), route); GivenClaimsAuthorizerReturns(new OkResponse(true)); // Act await _middleware.Invoke(_httpContext); // Assert ThenClaimsAuthorizerIsCalled(); } [Fact] [Trait("Feat", "100")] // https://github.com/ThreeMammals/Ocelot/issues/100 [Trait("PR", "104")] // https://github.com/ThreeMammals/Ocelot/pull/104 [Trait("Release", "1.4.5")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.4.5 public async Task Invoke_RouteIsAuthenticated_WhenScopesAuthorizerError_ShouldUpsertErrors() { // Arrange var route = new DownstreamRouteBuilder() .WithAuthenticationOptions(new(new("authScheme"))) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/test").Build()) .Build(); var response = new ErrorResponse(new ScopeNotAuthorizedError("No match")); GivenTheDownStreamRouteIs(new(), route); GivenScopesAuthorizerReturns(response); // Act await _middleware.Invoke(_httpContext); // Assert ThenScopesAuthorizerIsCalled(); #if DEBUG _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once); var warnings = GetWarnings(); Assert.Contains($"The '/test' route encountered authorization errors due to user scopes:{Environment.NewLine}ScopeNotAuthorizedError: No match", warnings); #endif var errors = _httpContext.Items.Errors(); Assert.NotEmpty(errors); Assert.Contains(response.Errors[0], errors); var actual = Assert.Single(errors); Assert.Same(response.Errors[0], actual); Assert.Equal("No match", actual.Message); } private void GivenTheDownStreamRouteIs(List templatePlaceholderNameAndValues, DownstreamRoute downstreamRoute) { _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(templatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); } private void GivenScopesAuthorizerReturns(Response expected) => _scopesAuthorizer .Setup(x => x.Authorize(It.IsAny(), It.IsAny>())) .Returns(expected); private void GivenClaimsAuthorizerReturns(Response expected) => _claimsAuthorizer .Setup(x => x.Authorize(It.IsAny(), It.IsAny>(), It.IsAny>())) .Returns(expected); private void ThenScopesAuthorizerIsCalled(Func times = null) => _scopesAuthorizer.Verify( x => x.Authorize(It.IsAny(), It.IsAny>()), times ?? Times.Once); private void ThenClaimsAuthorizerIsCalled(Func times = null) => _claimsAuthorizer.Verify( x => x.Authorize(It.IsAny(), It.IsAny>(), It.IsAny>()), times ?? Times.Once); } ================================================ FILE: test/Ocelot.UnitTests/Authorization/ClaimsAuthorizerTests.cs ================================================ using Ocelot.Authorization; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Authorization; public class ClaimsAuthorizerTests : UnitTest { private readonly ClaimsAuthorizer _claimsAuthorizer; private ClaimsPrincipal _claimsPrincipal; private Dictionary _requirement; private List _urlPathPlaceholderNameAndValues; private Response _result; public ClaimsAuthorizerTests() { _claimsAuthorizer = new ClaimsAuthorizer(new ClaimsParser()); } [Fact] public void Should_authorize_user() { // Arrange GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { new("UserType", "registered"), }))); GivenARouteClaimsRequirement(new Dictionary { {"UserType", "registered"}, }); // Act WhenICallTheAuthorizer(); // Assert ThenTheUserIsAuthorized(); } [Fact] public void Should_authorize_dynamic_user() { // Arrange GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { new("userid", "14"), }))); GivenARouteClaimsRequirement(new Dictionary { {"userid", "{userId}"}, }); GivenAPlaceHolderNameAndValueList(new List { new("{userId}", "14"), }); // Act WhenICallTheAuthorizer(); // Assert ThenTheUserIsAuthorized(); } [Fact] public void Should_not_authorize_dynamic_user() { // Arrange GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { new("userid", "15"), }))); GivenARouteClaimsRequirement(new Dictionary { {"userid", "{userId}"}, }); GivenAPlaceHolderNameAndValueList(new List { new("{userId}", "14"), }); // Act WhenICallTheAuthorizer(); // Assert ThenTheUserIsntAuthorized(); } [Fact] public void Should_authorize_user_multiple_claims_of_same_type() { // Arrange GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List { new("UserType", "guest"), new("UserType", "registered"), }))); GivenARouteClaimsRequirement(new Dictionary { {"UserType", "registered"}, }); // Act WhenICallTheAuthorizer(); // Assert ThenTheUserIsAuthorized(); } [Fact] public void Should_not_authorize_user() { // Arrange GivenAClaimsPrincipal(new ClaimsPrincipal(new ClaimsIdentity(new List()))); GivenARouteClaimsRequirement(new Dictionary { { "UserType", "registered" }, }); // Act WhenICallTheAuthorizer(); // Assert ThenTheUserIsntAuthorized(); } private void GivenAClaimsPrincipal(ClaimsPrincipal claimsPrincipal) { _claimsPrincipal = claimsPrincipal; } private void GivenARouteClaimsRequirement(Dictionary requirement) { _requirement = requirement; } private void GivenAPlaceHolderNameAndValueList(List urlPathPlaceholderNameAndValues) { _urlPathPlaceholderNameAndValues = urlPathPlaceholderNameAndValues; } private void WhenICallTheAuthorizer() { _result = _claimsAuthorizer.Authorize(_claimsPrincipal, _requirement, _urlPathPlaceholderNameAndValues); } private void ThenTheUserIsAuthorized() { _result.Data.ShouldBe(true); } private void ThenTheUserIsntAuthorized() { _result.Data.ShouldBe(false); } } ================================================ FILE: test/Ocelot.UnitTests/Authorization/UnauthorizedErrorTests.cs ================================================ using Ocelot.Authorization; using Ocelot.Errors; namespace Ocelot.UnitTests.Authorization; public class UnauthorizedErrorTests : UnitTest { [Fact] public void Ctor() { UnauthorizedError error = new(TestID); Assert.Equal(TestID, error.Message); Assert.Equal(OcelotErrorCode.UnauthorizedError, error.Code); Assert.Equal(403, error.HttpStatusCode); } } ================================================ FILE: test/Ocelot.UnitTests/Authorization/UserDoesNotHaveClaimErrorTests.cs ================================================ using Ocelot.Authorization; using Ocelot.Errors; namespace Ocelot.UnitTests.Authorization; public class UserDoesNotHaveClaimErrorTests : UnitTest { [Fact] public void Ctor() { UserDoesNotHaveClaimError error = new(TestID); Assert.Equal(TestID, error.Message); Assert.Equal(OcelotErrorCode.UserDoesNotHaveClaimError, error.Code); Assert.Equal(403, error.HttpStatusCode); } } ================================================ FILE: test/Ocelot.UnitTests/Cache/CacheOptionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Reflection; namespace Ocelot.UnitTests.Cache; public class CacheOptionsCreatorTests : UnitTest { private readonly CacheOptionsCreator _creator = new(); [Fact] public void Should_create_region_from_loadBalancingKey() { // Arrange var route = new FileRoute { FileCacheOptions = new() { Region = string.Empty, }, }; // Act var actual = _creator.Create(route, new FileGlobalConfiguration(), "testKey"); // Assert Assert.Equal("testKey", actual.Region); } [Fact] public void Should_use_region() { // Arrange var route = new FileRoute { FileCacheOptions = new() { Region = "region", }, }; // Act var actual = _creator.Create(route, new FileGlobalConfiguration(), "bla-bla"); // Assert Assert.Equal("region", actual.Region); } [Fact] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void ShouldCreateCacheOptions() { // Arrange var options = GivenCacheOptions(); var route = GivenRoute(options); // Act var result = _creator.Create(route, new(), null); // Assert result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); result.Region.ShouldBe(options.Region); result.Header.ShouldBe(options.Header); result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); } [Fact] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void ShouldCreateCacheOptionsUsingGlobalConfiguration() { // Arrange var global = GivenGlobalConfiguration(); var options = new FileCacheOptions(); var route = GivenRoute(options); // Act var result = _creator.Create(route, global, null); // Assert result.TtlSeconds.ShouldBe(global.CacheOptions.TtlSeconds.Value); result.Region.ShouldBe(global.CacheOptions.Region); result.Header.ShouldBe(global.CacheOptions.Header); result.EnableContentHashing.ShouldBe(global.CacheOptions.EnableContentHashing.Value); } [Fact] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void RouteCacheOptionsShouldOverrideGlobalConfiguration() { // Arrange var global = GivenGlobalConfiguration(); var options = GivenCacheOptions(); var route = GivenRoute(options); // Act var result = _creator.Create(route, global, null); // Assert result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); result.Region.ShouldBe(options.Region); result.Header.ShouldBe(options.Header); result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); } [Fact] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void ShouldCreateCacheOptionsWithDefaults() { // Arrange var options = new FileCacheOptions(); var route = GivenRoute(options); // Act var result = _creator.Create(route, new(), "testLbKey"); // Assert result.TtlSeconds.ShouldBe(0); result.Region.ShouldBe("testLbKey"); result.Header.ShouldBe("OC-Cache-Control"); result.EnableContentHashing.ShouldBe(false); } [Fact] [Trait("Feat", "2058")] [Trait("Bug", "2059")] public void ShouldComputeRegionIfNotProvided() { // Arrange var global = GivenGlobalConfiguration(); var options = GivenCacheOptions(); var route = GivenRoute(options); global.CacheOptions.Region = null; options.Region = null; // Act var result = _creator.Create(route, global, "testLbKey"); // Assert result.TtlSeconds.ShouldBe(options.TtlSeconds.Value); result.Region.ShouldBe("testLbKey"); result.Header.ShouldBe(options.Header); result.EnableContentHashing.ShouldBe(options.EnableContentHashing.Value); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void Create_FileCacheOptions() { // Arrange, Act, Assert : null FileCacheOptions options = null; var actual = _creator.Create(options); Assert.NotNull(actual); Assert.False(actual.UseCache); // Arrange, Act, Assert : not null options = GivenCacheOptions(); actual = _creator.Create(options); Assert.Equal(options.TtlSeconds.Value, actual.TtlSeconds); Assert.Equal(options.Region, actual.Region); Assert.Equal(options.Header, actual.Header); Assert.Equal(options.EnableContentHashing.Value, actual.EnableContentHashing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void Create_FromRoute_NullChecks() { // Arrange, Act, Assert FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration, "lbKey")); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration, "lbKey")); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void Create_FromRoute() { // Arrange FileRoute route = new() { FileCacheOptions = new() { TtlSeconds = 1, Region = "route", }, }; FileGlobalConfiguration globalConfiguration = new() { CacheOptions = new() { TtlSeconds = 33, Region = "global", }, }; // Act, Assert : from FileCacheOptions var actual = _creator.Create(route, globalConfiguration, "lbKey"); Assert.Equal(1, actual.TtlSeconds); Assert.Equal("route", actual.Region); Assert.Equal("OC-Cache-Control", actual.Header); Assert.False(actual.EnableContentHashing); // Arrange : from CacheOptions route.FileCacheOptions = null; route.CacheOptions = new() { TtlSeconds = 2, Region = "route", Header = "fromCacheOptions", EnableContentHashing = true, }; // Act, Assert : from CacheOptions actual = _creator.Create(route, globalConfiguration, "lbKey"); Assert.Equal(2, actual.TtlSeconds); Assert.Equal("route", actual.Region); Assert.Equal("fromCacheOptions", actual.Header); Assert.True(actual.EnableContentHashing); // Arrange, Act, Assert : from route if not in the group route.Key = "bla-bla"; globalConfiguration.CacheOptions.RouteKeys = ["R1"]; actual = _creator.Create(route, globalConfiguration, "lbKey"); Assert.Equal(2, actual.TtlSeconds); Assert.Equal("route", actual.Region); // Arrange, Act, Assert : from global route.CacheOptions = null; globalConfiguration.CacheOptions.RouteKeys.Clear(); actual = _creator.Create(route, globalConfiguration, "lbKey"); Assert.Equal(33, actual.TtlSeconds); Assert.Equal("global", actual.Region); Assert.Equal("OC-Cache-Control", actual.Header); Assert.False(actual.EnableContentHashing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void Create_FromDynamicRoute_NullChecks() { // Arrange, Act, Assert FileDynamicRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration, "lbKey")); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration, "lbKey")); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void Create_FromDynamicRoute() { // Arrange FileDynamicRoute route = new() { CacheOptions = new() { TtlSeconds = 1, Region = "route", Header = "route", EnableContentHashing = true, }, }; FileGlobalConfiguration globalConfiguration = new() { CacheOptions = new() { TtlSeconds = 2, Region = "global", Header = "global", }, }; // Act var actual = _creator.Create(route, globalConfiguration, "lbKey"); // Assert Assert.Equal(1, actual.TtlSeconds); Assert.Equal("route", actual.Region); Assert.Equal("route", actual.Header); Assert.True(actual.EnableContentHashing); // Arrange, Act, Assert : from global route.CacheOptions = null; actual = _creator.Create(route, globalConfiguration, "lbKey"); Assert.Equal(2, actual.TtlSeconds); Assert.Equal("global", actual.Region); Assert.Equal("global", actual.Header); Assert.False(actual.EnableContentHashing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void CreateProtected() { // Scenario 1: Null checks // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); IRouteGrouping grouping = null; FileCacheOptions options = null; FileGlobalCacheOptions globalOptions = null; string loadBalancingKey = "lbKey"; // Act var wrapper = Assert.Throws( () => method.Invoke(_creator, [grouping, options, globalOptions, loadBalancingKey])); // Assert : Null checks Assert.IsType(wrapper.InnerException); var actualEx = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(grouping), actualEx.ParamName); // Scenario 2: if-else branches FileDynamicRoute route = new() { Key = "r1" }; options = null; globalOptions = new() { RouteKeys = null, Region = "global", Header = "global", TtlSeconds = 3, }; // Act, Assert var actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); Assert.Equal("global", actual.Region); Assert.Equal("global", actual.Header); Assert.Equal(3, actual.TtlSeconds); // Arrange 2 options = new() { Region = "route", Header = "route", TtlSeconds = 1, }; globalOptions.RouteKeys = ["?"]; // Act, Assert 2 actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); Assert.Equal("route", actual.Region); Assert.Equal("route", actual.Header); Assert.Equal(1, actual.TtlSeconds); globalOptions.RouteKeys = ["r1"]; actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); Assert.Equal("route", actual.Region); Assert.Equal("route", actual.Header); Assert.Equal(1, actual.TtlSeconds); globalOptions = null; actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); Assert.Equal("route", actual.Region); Assert.Equal("route", actual.Header); Assert.Equal(1, actual.TtlSeconds); // Arrange 3 options.Header = null; globalOptions = new() { RouteKeys = null, Region = "global", Header = "global", TtlSeconds = 5, }; actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); Assert.Equal("route", actual.Region); Assert.Equal("global", actual.Header); Assert.Equal(1, actual.TtlSeconds); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public void CreateProtected_NoOptions() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new(); FileCacheOptions options = null; FileGlobalCacheOptions globalOptions = null; string loadBalancingKey = "lbKey"; // Act var actual = (CacheOptions)method.Invoke(_creator, [route, options, globalOptions, loadBalancingKey]); // Assert Assert.NotNull(actual); Assert.Equal(0, actual.TtlSeconds); Assert.Null(actual.Region); Assert.Null(actual.Header); Assert.False(actual.EnableContentHashing); Assert.False(actual.UseCache); } private static FileGlobalConfiguration GivenGlobalConfiguration() => new() { CacheOptions = new() { TtlSeconds = 20, Region = "globalRegion", Header = "globalHeader", EnableContentHashing = false, }, }; private static FileRoute GivenRoute(FileCacheOptions options) => new() { FileCacheOptions = options, }; private static FileCacheOptions GivenCacheOptions() => new() { TtlSeconds = 10, Region = "region", Header = "header", EnableContentHashing = true, }; } ================================================ FILE: test/Ocelot.UnitTests/Cache/CachedResponseTests.cs ================================================ using Ocelot.Cache; namespace Ocelot.UnitTests.Cache; public class CachedResponseTests { [Fact] public void Ctor() { // Arrange Dictionary> headers = new() { { "header", ["headerValue"] }, }; // Act CachedResponse actual = new(HttpStatusCode.Created, headers, "body", headers, "reasonPhrase"); // Assert Assert.Equal(HttpStatusCode.Created, actual.StatusCode); Assert.NotEmpty(actual.Headers); Assert.Contains("header", actual.Headers); Assert.NotEmpty(actual.ContentHeaders); Assert.Contains("header", actual.ContentHeaders); Assert.Equal("body", actual.Body); Assert.Equal("reasonPhrase", actual.ReasonPhrase); } [Fact] public void Ctor_Defaulting() { // Arrange, Act CachedResponse actual = new(HttpStatusCode.NotFound, null, null, null, "reasonPhrase"); // Assert Assert.Equal(HttpStatusCode.NotFound, actual.StatusCode); Assert.Empty(actual.Headers); Assert.Empty(actual.ContentHeaders); Assert.Empty(actual.Body); Assert.Equal("reasonPhrase", actual.ReasonPhrase); } } ================================================ FILE: test/Ocelot.UnitTests/Cache/DefaultCacheKeyGeneratorTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Cache; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Request.Middleware; using System.Reflection; using System.Text; namespace Ocelot.UnitTests.Cache; public sealed class DefaultCacheKeyGeneratorTests : UnitTest, IDisposable { private readonly DefaultCacheKeyGenerator _generator; private readonly HttpRequestMessage _request; private readonly string verb = HttpMethods.Get; private const string url = "https://some.url/blah?abcd=123"; private const string header = nameof(DefaultCacheKeyGeneratorTests); private const string headerName = "auth"; public DefaultCacheKeyGeneratorTests() { _generator = new DefaultCacheKeyGenerator(); _request = new HttpRequestMessage { Method = new(verb), RequestUri = new(url), }; _request.Headers.Add(headerName, header); } [Fact] public async Task Should_generate_cache_key_with_request_content() { // Arrange const string noHeader = null; const string content = nameof(Should_generate_cache_key_with_request_content); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}--{content}"); var options = new CacheOptions(100, "region", noHeader, true); var route = GivenDownstreamRoute(options); _request.Content = new StringContent(content); // Act var generatedCacheKey = await WhenGenerateRequestCacheKey(route); // Assert generatedCacheKey.ShouldBe(cachekey); } [Fact] public async Task Should_generate_cache_key_without_request_content() { // Arrange CacheOptions options = null; var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}"); var route = GivenDownstreamRoute(options); // Act var generatedCacheKey = await WhenGenerateRequestCacheKey(route); // Assert generatedCacheKey.ShouldBe(cachekey); } [Fact] public async Task Should_generate_cache_key_with_cache_options_header() { // Arrange var options = new CacheOptions(100, "region", headerName, false); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}"); var route = GivenDownstreamRoute(options); // Act var generatedCacheKey = await WhenGenerateRequestCacheKey(route); // Assert generatedCacheKey.ShouldBe(cachekey); // Scenario 2: No header _request.Headers.Clear(); cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-"); generatedCacheKey = await WhenGenerateRequestCacheKey(route); Assert.Equal(cachekey, generatedCacheKey); } [Fact] public async Task Should_generate_cache_key_happy_path() { // Arrange const string content = nameof(Should_generate_cache_key_happy_path); var options = new CacheOptions(100, "region", headerName, true); var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}-{content}"); var route = GivenDownstreamRoute(options); _request.Content = new StringContent(content); // Act var generatedCacheKey = await WhenGenerateRequestCacheKey(route); // Assert generatedCacheKey.ShouldBe(cachekey); } private static DownstreamRoute GivenDownstreamRoute(CacheOptions options) => new DownstreamRouteBuilder() .WithKey("key1") .WithCacheOptions(options) .Build(); private ValueTask WhenGenerateRequestCacheKey(DownstreamRoute route) => _generator.GenerateRequestCacheKey(new DownstreamRequest(_request), route); public void Dispose() { _request.Dispose(); } } internal class HttpContentStub : HttpContent { private readonly MemoryStream _stream; public HttpContentStub(string content) { _stream = new MemoryStream(Encoding.ASCII.GetBytes(content)); var field = typeof(HttpContent).GetField("_bufferedContent", BindingFlags.NonPublic | BindingFlags.Instance); field.SetValue(this, _stream); } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) => throw new NotImplementedException(); protected override bool TryComputeLength(out long length) => throw new NotImplementedException(); } ================================================ FILE: test/Ocelot.UnitTests/Cache/DefaultMemoryCacheTests.cs ================================================ using Microsoft.Extensions.Caching.Memory; using Ocelot.Cache; namespace Ocelot.UnitTests.Cache; public class DefaultMemoryCacheTests : UnitTest { private readonly DefaultMemoryCache _cache; public DefaultMemoryCacheTests() { _ttl = TimeSpan.FromSeconds(100); _cache = new DefaultMemoryCache(new MemoryCache(new MemoryCacheOptions())); } protected TimeSpan TTL => _ttl; private TimeSpan _ttl; [Fact] public void Should_cache() { // Arrange var fake = new Fake(1); _cache.Add("1", fake, "region", TTL); // Act var result = _cache.Get("1", "region"); // Assert result.ShouldBe(fake); fake.Value.ShouldBe(1); } [Fact] public void Doesnt_exist() { // Arrange, Act var result = _cache.Get("1", "region"); // Assert result.ShouldBeNull(); } [Fact] public void Should_add_or_update() { // Arrange var fake = new Fake(1); _cache.Add("1", fake, "region", TTL); var newFake = new Fake(1); _cache.AddOrUpdate("1", newFake, "region", TTL); // Act var result = _cache.Get("1", "region"); // Assert result.ShouldBe(newFake); newFake.Value.ShouldBe(1); } [Fact] public void Should_clear_region() { // Arrange var fake1 = new Fake(1); var fake2 = new Fake(2); _cache.Add("1", fake1, "region", TTL); _cache.Add("2", fake2, "region", TTL); _cache.ClearRegion("region"); // Act, Assert var result1 = _cache.Get("1", "region"); result1.ShouldBeNull(); // Act, Assert var result2 = _cache.Get("2", "region"); result2.ShouldBeNull(); } [Fact] public void Should_clear_key_if_ttl_expired() { // Arrange var fake = new Fake(1); _cache.Add("1", fake, "region", TimeSpan.FromMilliseconds(50)); Thread.Sleep(200); // Act var result = _cache.Get("1", "region"); // Assert result.ShouldBeNull(); } [Theory] [InlineData(0)] [InlineData(-1)] public void Should_not_add_to_cache_if_timespan_empty(int ttl) { // Arrange var fake = new Fake(1); _cache.Add("1", fake, "region", TimeSpan.FromSeconds(ttl)); // Act var result = _cache.Get("1", "region"); // Assert result.ShouldBeNull(); } private class Fake { public Fake(int value) { Value = value; } public int Value { get; } } } ================================================ FILE: test/Ocelot.UnitTests/Cache/FileCacheOptionsTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Cache; public class FileCacheOptionsTests { [Fact] public void Ctor_int() { var actual = new FileCacheOptions(3); Assert.Equal(3, actual.TtlSeconds); Assert.Null(actual.Region); Assert.Null(actual.Header); Assert.Null(actual.EnableContentHashing); } [Fact] public void Ctor_FileCacheOptions() { FileCacheOptions from = new() { TtlSeconds = 4, Region = "region", Header = "header", EnableContentHashing = true, }; FileCacheOptions actual = new(from); Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); Assert.Equal(4, actual.TtlSeconds); } } ================================================ FILE: test/Ocelot.UnitTests/Cache/FileGlobalCacheOptionsTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Cache; [Trait("Feat", "585")] [Trait("Feat", "2330")] // https://github.com/ThreeMammals/Ocelot/issues/2330 public class FileGlobalCacheOptionsTests { [Fact] public void Ctor_int() { var actual = new FileGlobalCacheOptions(3); Assert.Equal(3, actual.TtlSeconds); Assert.Null(actual.Region); Assert.Null(actual.Header); Assert.Null(actual.EnableContentHashing); } [Fact] public void Ctor_FileCacheOptions() { FileCacheOptions from = new() { TtlSeconds = 4, Region = "region", Header = "header", EnableContentHashing = true, }; FileGlobalCacheOptions actual = new(from); Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); Assert.Equal(4, actual.TtlSeconds); Assert.Null(actual.RouteKeys); } } ================================================ FILE: test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Cache; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester; namespace Ocelot.UnitTests.Cache; public class OutputCacheMiddlewareTests : UnitTest { private readonly Mock> _cache = new(); private readonly Mock _loggerFactory = new(); private readonly Mock _logger = new(); private readonly Mock _cacheGenerator = new(); private OutputCacheMiddleware _middleware; private RequestDelegate _next; private readonly ICacheKeyGenerator _cacheKeyGenerator; private CachedResponse _response; private readonly DefaultHttpContext _httpContext; Func _message; public OutputCacheMiddlewareTests() { _httpContext = new DefaultHttpContext(); _cacheKeyGenerator = new DefaultCacheKeyGenerator(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _logger.Setup(x => x.LogDebug(It.IsAny>())) .Callback>(m => _message = m); _cacheGenerator.Setup(x => x.GenerateRequestCacheKey(It.IsAny(), It.IsAny())) .Returns((req, rou) => _cacheKeyGenerator.GenerateRequestCacheKey(req, rou)); _next = context => Task.CompletedTask; _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"))); } [Fact] public async Task Should_returned_cached_item_when_it_is_in_cache() { // Arrange var headers = new Dictionary> { { "test", new List { "test" } }, }; var contentHeaders = new Dictionary> { { "content-type", new List { "application/json" } }, }; var cachedResponse = new CachedResponse(HttpStatusCode.OK, headers, string.Empty, contentHeaders, "some reason"); GivenThereIsACachedResponse(cachedResponse); GivenTheDownstreamRouteIs(); // Act await WhenICallTheMiddlewareAsync(); // Assert ThenTheCacheGetIsCalledCorrectly(); } [Fact] public async Task Should_returned_cached_item_when_it_is_in_cache_expires_header() { // Arrange var contentHeaders = new Dictionary> { { "Expires", new List { "-1" } }, }; var cachedResponse = new CachedResponse(HttpStatusCode.OK, new Dictionary>(), string.Empty, contentHeaders, "some reason"); GivenThereIsACachedResponse(cachedResponse); GivenTheDownstreamRouteIs(); // Act await WhenICallTheMiddlewareAsync(); // Assert ThenTheCacheGetIsCalledCorrectly(); } [Fact] public async Task Should_continue_with_pipeline_and_cache_response() { // Arrange GivenResponseIsNotCached(new HttpResponseMessage()); GivenTheDownstreamRouteIs(); // Act await WhenICallTheMiddlewareAsync(); // Assert ThenTheCacheAddIsCalled(); } [Fact] public async Task Should_not_use_cache() { // Arrange GivenResponseIsNotCached(new HttpResponseMessage()); GivenTheDownstreamRouteIs(new CacheOptions(0, null, null, null)); // Act await WhenICallTheMiddlewareAsync(); // Assert _cacheGenerator.Verify( x => x.GenerateRequestCacheKey(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public async Task Should_not_add_to_cache_when_errors() { // Arrange GivenResponseIsNotCached(new HttpResponseMessage()); GivenTheDownstreamRouteIs(); _next = static context => { context.Items.UpsertErrors([new RequestCanceledError("Bla-bla message")]); return Task.CompletedTask; }; // Act await WhenICallTheMiddlewareAsync(); // Assert ThenTheCacheAddIsCalled(Times.Never); ThenTheMessageIs("There was a pipeline error for the 'GET-https://some.url/blah?abcd=123' key."); } [Fact] public async Task CreateHttpResponseMessage_CachedIsNull() { // Arrange CachedResponse cached = null; GivenThereIsACachedResponse(cached); GivenTheDownstreamRouteIs(); // Act await WhenICallTheMiddlewareAsync(); // Assert ThenTheCacheGetIsCalledCorrectly(); } private void ThenTheMessageIs(string expected) { Assert.NotNull(_message); var msg = _message.Invoke(); Assert.Equal(expected, msg); } private async Task WhenICallTheMiddlewareAsync() { _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheGenerator.Object); await _middleware.Invoke(_httpContext); } private void GivenThereIsACachedResponse(CachedResponse response) { _response = response; _cache .Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(_response); } private void GivenResponseIsNotCached(HttpResponseMessage responseMessage) { _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(responseMessage)); } private void GivenTheDownstreamRouteIs(CacheOptions options = null) { var downRoute = new DownstreamRouteBuilder() .WithCacheOptions(options ?? new(100, "kanken", null, false)) .WithUpstreamHttpMethod([ "Get" ]) .Build(); var route = new Route(downRoute) { UpstreamHttpMethod = [HttpMethod.Get], }; var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void ThenTheCacheGetIsCalledCorrectly() { _cache.Verify( x => x.Get(It.IsAny(), It.IsAny()), Times.Once); } private void ThenTheCacheAddIsCalled(Func howMany = null) { _cache.Verify( x => x.Add(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), howMany ?? Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Claims/AddClaimsToRequestTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Claims; using Ocelot.Configuration; using Ocelot.Errors; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Claims; public class AddClaimsToRequestTests : UnitTest { private readonly AddClaimsToRequest _addClaimsToRequest; private readonly Mock _parser; private List _claimsToThings; private HttpContext _context; private Response _result; private Response _claimValue; public AddClaimsToRequestTests() { _parser = new Mock(); _addClaimsToRequest = new AddClaimsToRequest(_parser.Object); } [Fact] public void Should_add_claims_to_context() { // Arrange var context = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new List { new("test", "data"), })), }; GivenClaimsToThings(new List { new("claim-key", string.Empty, string.Empty, 0), }); GivenHttpContext(context); GivenTheClaimParserReturns(new OkResponse("value")); // Act WhenIAddClaimsToTheRequest(); // Assert ThenTheResultIsSuccess(); } [Fact] public void If_claims_exists_should_replace_it() { // Arrange var context = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(new List { new("existing-key", "data"), new("new-key", "data"), })), }; GivenClaimsToThings(new List { new("existing-key", "new-key", string.Empty, 0), }); GivenHttpContext(context); GivenTheClaimParserReturns(new OkResponse("value")); // Act WhenIAddClaimsToTheRequest(); // Assert ThenTheResultIsSuccess(); } [Fact] public void Should_return_error() { // Arrange GivenClaimsToThings(new List { new(string.Empty, string.Empty, string.Empty, 0), }); GivenHttpContext(new DefaultHttpContext()); GivenTheClaimParserReturns(new ErrorResponse(new List { new AnyError(), })); // Act WhenIAddClaimsToTheRequest(); // Assert ThenTheResultIsError(); } private void GivenClaimsToThings(List configuration) { _claimsToThings = configuration; } private void GivenHttpContext(HttpContext context) { _context = context; } private void GivenTheClaimParserReturns(Response claimValue) { _claimValue = claimValue; _parser .Setup( x => x.GetValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_claimValue); } private void WhenIAddClaimsToTheRequest() { _result = _addClaimsToRequest.SetClaimsOnContext(_claimsToThings, _context); } private void ThenTheResultIsSuccess() { _result.IsError.ShouldBe(false); } private void ThenTheResultIsError() { _result.IsError.ShouldBe(true); } private class AnyError : Error { public AnyError() : base("blahh", OcelotErrorCode.UnknownError, 404) { } } } ================================================ FILE: test/Ocelot.UnitTests/Claims/ClaimsToClaimsMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Claims; using Ocelot.Claims.Middleware; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.UnitTests.Claims; public class ClaimsToClaimsMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ClaimsToClaimsMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ClaimsToClaimsMiddlewareTests() { _httpContext = new DefaultHttpContext(); _addHeaders = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new ClaimsToClaimsMiddleware(_next, _loggerFactory.Object, _addHeaders.Object); } [Fact] public async Task Should_call_claims_to_request_correctly() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToClaims(new() { new("sub", "UserType", "|", 0), }) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route() { DownstreamRoute = [route], UpstreamHttpMethod = [HttpMethod.Get], }); GivenTheDownStreamRouteIs(downstreamRoute); GivenTheAddClaimsToRequestReturns(); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheClaimsToRequestIsCalledCorrectly(); } private void GivenTheDownStreamRouteIs(Ocelot.DownstreamRouteFinder.DownstreamRouteHolder downstreamRoute) { _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void GivenTheAddClaimsToRequestReturns() { _addHeaders .Setup(x => x.SetClaimsOnContext(It.IsAny>(), It.IsAny())) .Returns(new OkResponse()); } private void ThenTheClaimsToRequestIsCalledCorrectly() { _addHeaders .Verify(x => x.SetClaimsOnContext(It.IsAny>(), It.IsAny()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/AggregatesCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Values; namespace Ocelot.UnitTests.Configuration; public class AggregatesCreatorTests : UnitTest { private readonly AggregatesCreator _creator; private readonly Mock _utpCreator; private readonly Mock _uhtpCreator; private FileConfiguration _fileConfiguration; private List _routes; private List _result; private UpstreamPathTemplate _aggregate1Utp; private UpstreamPathTemplate _aggregate2Utp; private Dictionary _headerTemplates1; private Dictionary _headerTemplates2; public AggregatesCreatorTests() { _utpCreator = new Mock(); _uhtpCreator = new Mock(); _creator = new AggregatesCreator(_utpCreator.Object, _uhtpCreator.Object); } [Fact] public void Should_return_no_aggregates() { // Arrange var fileConfig = new FileConfiguration { Aggregates = new List { new() { RouteKeys = new(["key1"]), }, }, }; var routes = new List(); GivenThe(fileConfig); GivenThe(routes); // Act WhenICreate(); // Assert TheUtpCreatorIsNotCalled(); ThenTheResultIsNotNull(); ThenTheResultIsEmpty(); } [Fact] public void Should_create_aggregates() { // Arrange var fileConfig = new FileConfiguration { Aggregates = new List { new() { RouteKeys = new(["key1", "key2"]), UpstreamHost = "hosty", UpstreamPathTemplate = "templatey", Aggregator = "aggregatory", RouteIsCaseSensitive = true, }, new() { RouteKeys = new(["key3", "key4"]), UpstreamHost = "hosty", UpstreamPathTemplate = "templatey", Aggregator = "aggregatory", RouteIsCaseSensitive = true, }, }, }; var routes = new List { new(new DownstreamRouteBuilder().WithKey("key1").Build()), new(new DownstreamRouteBuilder().WithKey("key2").Build()), new(new DownstreamRouteBuilder().WithKey("key3").Build()), new(new DownstreamRouteBuilder().WithKey("key4").Build()), }; GivenThe(fileConfig); GivenThe(routes); GivenTheUtpCreatorReturns(); GivenTheUhtpCreatorReturns(); // Act WhenICreate(); // Assert ThenTheUtpCreatorIsCalledCorrectly(); ThenTheAggregatesAreCreated(); } private void ThenTheAggregatesAreCreated() { _result.ShouldNotBeNull(); _result.Count.ShouldBe(2); _result[0].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[0].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[0].UpstreamHost); _result[0].UpstreamTemplatePattern.ShouldBe(_aggregate1Utp); _result[0].UpstreamHeaderTemplates.ShouldBe(_headerTemplates1); _result[0].Aggregator.ShouldBe(_fileConfiguration.Aggregates[0].Aggregator); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[0].DownstreamRoute[0]); _result[0].DownstreamRoute.ShouldContain(x => x == _routes[1].DownstreamRoute[0]); _result[1].UpstreamHttpMethod.ShouldContain(x => x == HttpMethod.Get); _result[1].UpstreamHost.ShouldBe(_fileConfiguration.Aggregates[1].UpstreamHost); _result[1].UpstreamTemplatePattern.ShouldBe(_aggregate2Utp); _result[1].UpstreamHeaderTemplates.ShouldBe(_headerTemplates2); _result[1].Aggregator.ShouldBe(_fileConfiguration.Aggregates[1].Aggregator); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[2].DownstreamRoute[0]); _result[1].DownstreamRoute.ShouldContain(x => x == _routes[3].DownstreamRoute[0]); } private void ThenTheUtpCreatorIsCalledCorrectly() { _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[0]), Times.Once); _utpCreator.Verify(x => x.Create(_fileConfiguration.Aggregates[1]), Times.Once); } private void GivenTheUtpCreatorReturns() { _aggregate1Utp = new UpstreamPathTemplateBuilder().Build(); _aggregate2Utp = new UpstreamPathTemplateBuilder().Build(); _utpCreator.SetupSequence(x => x.Create(It.IsAny())) .Returns(_aggregate1Utp) .Returns(_aggregate2Utp); } private void GivenTheUhtpCreatorReturns() { _headerTemplates1 = new Dictionary(); _headerTemplates2 = new Dictionary(); _uhtpCreator.SetupSequence(x => x.Create(It.IsAny())) .Returns(_headerTemplates1) .Returns(_headerTemplates2); } private void ThenTheResultIsEmpty() { _result.Count.ShouldBe(0); } private void ThenTheResultIsNotNull() { _result.ShouldNotBeNull(); } private void TheUtpCreatorIsNotCalled() { _utpCreator.Verify(x => x.Create(It.IsAny()), Times.Never); } private void GivenThe(FileConfiguration fileConfiguration) { _fileConfiguration = fileConfiguration; } private void GivenThe(List routes) { _routes = routes; } private void WhenICreate() { _result = _creator.Create(_fileConfiguration, _routes); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenSourceTests.cs ================================================ using Ocelot.Configuration.ChangeTracking; namespace Ocelot.UnitTests.Configuration.ChangeTracking; public class OcelotConfigurationChangeTokenSourceTests : UnitTest { private readonly OcelotConfigurationChangeTokenSource _source; public OcelotConfigurationChangeTokenSourceTests() { _source = new OcelotConfigurationChangeTokenSource(); } [Fact] public void Should_activate_change_token() { // Arrange, Act _source.Activate(); // Assert _source.ChangeToken.HasChanged.ShouldBeTrue(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationChangeTokenTests.cs ================================================ using Ocelot.Configuration.ChangeTracking; namespace Ocelot.UnitTests.Configuration.ChangeTracking; public class OcelotConfigurationChangeTokenTests : UnitTest { [Fact] public void Should_call_callback_with_state() { // Arrange GivenIHaveAChangeToken(); AndIRegisterACallback(); ThenIShouldGetADisposableWrapper(); // Act GivenIActivateTheToken(); // Assert ThenTheCallbackShouldBeCalled(); } [Fact] public void Should_not_call_callback_if_it_is_disposed() { // Arrange GivenIHaveAChangeToken(); AndIRegisterACallback(); ThenIShouldGetADisposableWrapper(); // Act, Assert GivenIActivateTheToken(); AndIDisposeTheCallbackWrapper(); // Act, Assert GivenIActivateTheToken(); ThenTheCallbackShouldNotBeCalled(); } private OcelotConfigurationChangeToken _changeToken; private IDisposable _callbackWrapper; private int _callbackCounter; private readonly object _callbackInitialState = new(); private object _callbackState; private void Callback(object state) { _callbackCounter++; _callbackState = state; _changeToken.HasChanged.ShouldBeTrue(); } private void GivenIHaveAChangeToken() { _changeToken = new OcelotConfigurationChangeToken(); } private void AndIRegisterACallback() { _callbackWrapper = _changeToken.RegisterChangeCallback(Callback, _callbackInitialState); } private void ThenIShouldGetADisposableWrapper() { _callbackWrapper.ShouldNotBeNull(); } private void GivenIActivateTheToken() { _callbackCounter = 0; _callbackState = null; _changeToken.Activate(); } private void ThenTheCallbackShouldBeCalled() { _callbackCounter.ShouldBe(1); _callbackState.ShouldNotBeNull(); _callbackState.ShouldBeSameAs(_callbackInitialState); } private void ThenTheCallbackShouldNotBeCalled() { _callbackCounter.ShouldBe(0); _callbackState.ShouldBeNull(); } private void AndIDisposeTheCallbackWrapper() { _callbackState = null; _callbackCounter = 0; _callbackWrapper.Dispose(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ChangeTracking/OcelotConfigurationMonitorTests.cs ================================================ using Microsoft.Extensions.Primitives; using Ocelot.Configuration; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Repository; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration.ChangeTracking; public class OcelotConfigurationMonitorTests : UnitTest { private Mock _mConfigurationRepo; private Mock _mChangeTokenSource; private Mock _mChangeToken; private IInternalConfiguration _testConfiguration; private Response _repoResponse; private OcelotConfigurationMonitor _monitor; public OcelotConfigurationMonitorTests() { GivenTheRepositoryMock(); GivenTheChangeTokenSourceMock(); GivenTheChangeTokenMock(); } [Fact] public void Constructor_WithValidDependencies_ShouldCreateInstance() { // Arrange var repo = _mConfigurationRepo.Object; var changeTokenSource = _mChangeTokenSource.Object; // Act var monitor = new OcelotConfigurationMonitor(repo, changeTokenSource); // Assert Assert.NotNull(monitor); Assert.IsType(monitor); } [Fact] public void Constructor_WithNullRepository_ShouldThrowArgumentNullException() { // Arrange IInternalConfigurationRepository repo = null; // Act & Assert var e = Assert.Throws( () => new OcelotConfigurationMonitor(repo, _mChangeTokenSource.Object)); // Assert Assert.NotNull(e); Assert.Equal(nameof(repo), e.ParamName); } [Fact] public void Constructor_WithNullChangeTokenSource_ShouldThrowArgumentNullException() { // Arrange IOcelotConfigurationChangeTokenSource changeTokenSource = null; // Act & Assert var e = Assert.Throws( () => new OcelotConfigurationMonitor(_mConfigurationRepo.Object, changeTokenSource)); // Assert Assert.NotNull(e); Assert.Equal(nameof(changeTokenSource), e.ParamName); } [Fact] public void CurrentValue_WhenCalled_ShouldReturnConfigurationFromRepository() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); // Act var result = _monitor.CurrentValue; // Assert Assert.NotNull(result); Assert.Same(_testConfiguration, result); _mConfigurationRepo.Verify(x => x.Get(), Times.Once); } [Fact] public void Get_WithValidName_ShouldReturnConfigurationFromRepository() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); const string configName = "test-config"; // Act var result = _monitor.Get(configName); // Assert Assert.NotNull(result); Assert.Same(_testConfiguration, result); _mConfigurationRepo.Verify(x => x.Get(), Times.Once); } [Fact] public void Get_WithEmptyName_ShouldReturnConfigurationFromRepository() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); // Act var result = _monitor.Get(string.Empty); // Assert Assert.NotNull(result); Assert.Same(_testConfiguration, result); _mConfigurationRepo.Verify(x => x.Get(), Times.Once); } [Fact] public void Get_WithNullName_ShouldReturnConfigurationFromRepository() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); // Act var result = _monitor.Get(null); // Assert Assert.NotNull(result); Assert.Same(_testConfiguration, result); _mConfigurationRepo.Verify(x => x.Get(), Times.Once); } [Fact] public void OnChange_WithValidListener_ShouldRegisterCallbackOnChangeToken() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); var callbackInvoked = false; void listener(IInternalConfiguration config, string name) { callbackInvoked = true; } // Act var disposable = _monitor.OnChange(listener); // Assert Assert.NotNull(disposable); _mChangeToken.Verify( x => x.RegisterChangeCallback(It.IsAny>(), It.IsAny()), Times.Once ); Assert.False(callbackInvoked); } [Fact] public void OnChange_ShouldReturnDisposableWrapper() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); var mockDisposable = new Mock(); _mChangeToken .Setup(x => x.RegisterChangeCallback(It.IsAny>(), It.IsAny())) .Returns(mockDisposable.Object); // Act var result = _monitor.OnChange((config, name) => { }); // Assert Assert.NotNull(result); Assert.Same(mockDisposable.Object, result); } [Fact] public void OnChange_WhenChangeTokenCallbackIsInvoked_ShouldCallListenerWithCurrentValueAndEmptyString() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); IInternalConfiguration capturedConfig = null; string capturedName = null; void listener(IInternalConfiguration config, string name) { capturedConfig = config; capturedName = name; } Action capturedCallback = null; _mChangeToken .Setup(x => x.RegisterChangeCallback(It.IsAny>(), It.IsAny())) .Callback, object>((callback, state) => capturedCallback = callback) .Returns(new Mock().Object); // Act _monitor.OnChange(listener); capturedCallback?.Invoke(null); // Assert Assert.NotNull(capturedConfig); Assert.Same(_testConfiguration, capturedConfig); Assert.Equal(string.Empty, capturedName); } [Fact] public void OnChange_WithNullListener_ShouldThrowArgumentNullException() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); Action listener = null; // Act & Assert var e = Assert.Throws( () => _monitor.OnChange(listener)); // Assert Assert.NotNull(e); Assert.Equal(nameof(listener), e.ParamName); } [Fact] public void CurrentValue_MultipleInvocations_ShouldCallRepositoryEachTime() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); // Act _ = _monitor.CurrentValue; _ = _monitor.CurrentValue; _ = _monitor.CurrentValue; // Assert _mConfigurationRepo.Verify(x => x.Get(), Times.Exactly(3)); } [Fact] public void Get_MultipleInvocationsWithDifferentNames_ShouldCallRepositoryEachTime() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); // Act _ = _monitor.Get("config1"); _ = _monitor.Get("config2"); _ = _monitor.Get("config3"); // Assert _mConfigurationRepo.Verify(x => x.Get(), Times.Exactly(3)); } [Fact] public void OnChange_MultipleListenersRegistered_ShouldRegisterAllCallbacks() { // Arrange GivenATestConfigurationIsSet(); GivenTheMonitorIsCreated(); var listener1Called = false; var listener2Called = false; // Act _monitor.OnChange((config, name) => listener1Called = true); _monitor.OnChange((config, name) => listener2Called = true); // Assert _mChangeToken.Verify( x => x.RegisterChangeCallback(It.IsAny>(), It.IsAny()), Times.Exactly(2)); Assert.False(listener1Called); Assert.False(listener2Called); } // Helper methods private void GivenTheRepositoryMock() { _mConfigurationRepo = new Mock(); } private void GivenTheChangeTokenSourceMock() { _mChangeTokenSource = new Mock(); _mChangeTokenSource.Setup(x => x.ChangeToken).Returns(_mChangeToken?.Object ?? new Mock().Object); } private void GivenTheChangeTokenMock() { _mChangeToken = new Mock(); _mChangeToken .Setup(x => x.RegisterChangeCallback(It.IsAny>(), It.IsAny())) .Returns(new Mock().Object); } private void GivenATestConfigurationIsSet() { _testConfiguration = new Mock().Object; _repoResponse = new OkResponse(_testConfiguration); _mConfigurationRepo.Setup(x => x.Get()).Returns(_repoResponse); } private void GivenTheMonitorIsCreated() { _mChangeTokenSource.Setup(x => x.ChangeToken).Returns(_mChangeToken.Object); _monitor = new OcelotConfigurationMonitor(_mConfigurationRepo.Object, _mChangeTokenSource.Object); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ClaimToThingConfigurationParserTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Parser; using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration; /// /// Feature: Claims to Headers. /// [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class ClaimToThingConfigurationParserTests : UnitTest { private readonly ClaimToThingConfigurationParser _parser; public ClaimToThingConfigurationParserTests() { _parser = new ClaimToThingConfigurationParser(); } [Fact] public void Returns_no_instructions_error() { // Arrange var dictionary = new Dictionary { {"CustomerId", string.Empty}, }; // Act var result = WhenICallTheExtractor(dictionary); // Assert ThenAnErrorIsReturned(result, new ErrorResponse( new List { new NoInstructionsError(">"), })); } [Fact] public void Returns_no_instructions_not_for_claims_error() { // Arrange var dictionary = new Dictionary { {"CustomerId", "Cheese[CustomerId] > value"}, }; // Act var result = WhenICallTheExtractor(dictionary); // Assert ThenAnErrorIsReturned(result, new ErrorResponse(new List { new InstructionNotForClaimsError(), })); } [Fact] public void Can_parse_entry_to_work_out_properties_with_key() { // Arrange var dictionary = new Dictionary { {"CustomerId", "Claims[CustomerId] > value"}, }; // Act var result = WhenICallTheExtractor(dictionary); // Assert ThenTheClaimParserPropertiesAreReturned(result, new OkResponse(new ClaimToThing("CustomerId", "CustomerId", string.Empty, 0))); } [Fact] public void Can_parse_entry_to_work_out_properties_with_key_delimiter_and_index() { // Arrange var dictionary = new Dictionary { {"UserId", "Claims[Subject] > value[0] > |"}, }; // Act var result = WhenICallTheExtractor(dictionary); // Assert ThenTheClaimParserPropertiesAreReturned(result, new OkResponse(new ClaimToThing("UserId", "Subject", "|", 0))); } private static void ThenAnErrorIsReturned(Response result, Response expected) { result.IsError.ShouldBe(expected.IsError); result.Errors[0].ShouldBeOfType(expected.Errors[0].GetType()); } private static void ThenTheClaimParserPropertiesAreReturned(Response result, Response expected) { result.Data.NewKey.ShouldBe(expected.Data.NewKey); result.Data.Delimiter.ShouldBe(expected.Data.Delimiter); result.Data.Index.ShouldBe(expected.Data.Index); result.IsError.ShouldBe(expected.IsError); } private Response WhenICallTheExtractor(Dictionary dictionary) { var first = dictionary.First(); return _parser.Extract(first.Key, first.Value); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.Parser; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration; public class ClaimsToThingCreatorTests : UnitTest { private readonly Mock _configParser; private Dictionary _claimsToThings; private readonly ClaimsToThingCreator _claimsToThingsCreator; private readonly Mock _loggerFactory; private List _result; private readonly Mock _logger; public ClaimsToThingCreatorTests() { _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory .Setup(x => x.CreateLogger()) .Returns(_logger.Object); _configParser = new Mock(); _claimsToThingsCreator = new ClaimsToThingCreator(_configParser.Object, _loggerFactory.Object); } [Fact] public void Should_return_claims_to_things() { // Arrange var userInput = new Dictionary { {"CustomerId", "Claims[CustomerId] > value"}, }; var claimsToThing = new OkResponse(new ClaimToThing("CustomerId", "CustomerId", string.Empty, 0)); GivenTheFollowingDictionary(userInput); GivenTheConfigHeaderExtractorReturns(claimsToThing); // Act WhenIGetTheThings(); // Assert ThenTheConfigParserIsCalledCorrectly(); ThenClaimsToThingsAreReturned(); } [Fact] public void Should_log_error_if_cannot_parse_claim_to_thing() { // Arrange var userInput = new Dictionary { {"CustomerId", "Claims[CustomerId] > value"}, }; var claimsToThing = new ErrorResponse(It.IsAny()); GivenTheFollowingDictionary(userInput); GivenTheConfigHeaderExtractorReturns(claimsToThing); // Act WhenIGetTheThings(); // Assert ThenTheConfigParserIsCalledCorrectly(); ThenNoClaimsToThingsAreReturned(); } private void ThenClaimsToThingsAreReturned() { _result.Count.ShouldBeGreaterThan(0); } private void GivenTheFollowingDictionary(Dictionary claimsToThings) { _claimsToThings = claimsToThings; } private void GivenTheConfigHeaderExtractorReturns(Response expected) { _configParser .Setup(x => x.Extract(It.IsAny(), It.IsAny())) .Returns(expected); } private void ThenNoClaimsToThingsAreReturned() { _result.Count.ShouldBe(0); } private void WhenIGetTheThings() { _result = _claimsToThingsCreator.Create(_claimsToThings); } private void ThenTheConfigParserIsCalledCorrectly() { _configParser .Verify(x => x.Extract(_claimsToThings.First().Key, _claimsToThings.First().Value), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ConfigurationCreatorTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Administration; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Configuration; public class ConfigurationCreatorTests : UnitTest { private ConfigurationCreator _creator; private InternalConfiguration _result; private readonly Mock _spcCreator; private readonly Mock _qosCreator; private readonly Mock _hhoCreator; private readonly Mock _lboCreator; private readonly Mock _vCreator; private readonly Mock _vpCreator; private readonly Mock _mdCreator; private readonly Mock _rlCreator; private readonly Mock _coCreator; private readonly Mock _authCreator; private FileConfiguration _fileConfig; private Route[] _routes; private ServiceProviderConfiguration _spc; private LoadBalancerOptions _lbo; private QoSOptions _qoso; private HttpHandlerOptions _hho; private CacheOptions _co; private AuthenticationOptions _ao; private AdministrationPath _adminPath; private readonly ServiceCollection _serviceCollection; public ConfigurationCreatorTests() { _vCreator = new Mock(); _vpCreator = new Mock(); _lboCreator = new Mock(); _hhoCreator = new Mock(); _qosCreator = new Mock(); _spcCreator = new Mock(); _mdCreator = new Mock(); _rlCreator = new Mock(); _coCreator = new Mock(); _authCreator = new Mock(); _serviceCollection = new ServiceCollection(); } [Fact] public void Should_build_configuration_with_no_admin_path() { // Arrange GivenTheDependenciesAreSetUp(); // Act WhenICreate(); // Assert ThenTheDepdenciesAreCalledCorrectly(); ThenThePropertiesAreSetCorrectly(); _result.AdministrationPath.ShouldBeNull(); } [Fact] public void Should_build_configuration_with_admin_path() { // Arrange GivenTheDependenciesAreSetUp(); GivenTheAdminPath(); // Act WhenICreate(); // Assert ThenTheDepdenciesAreCalledCorrectly(); ThenThePropertiesAreSetCorrectly(); ThenTheAdminPathIsSet(); } [Fact] public void Configuration_GlobalConfiguration_SoftNullGuard() { // Arrange GivenTheDependenciesAreSetUp(); _fileConfig.GlobalConfiguration = null; // Act WhenICreate(); // Assert ThenTheDepdenciesAreCalledCorrectly(); ThenThePropertiesAreSetCorrectly(); } private void ThenThePropertiesAreSetCorrectly() { _fileConfig.GlobalConfiguration ??= new(); _result.ShouldNotBeNull(); _result.ServiceProviderConfiguration.ShouldBe(_spc); _result.LoadBalancerOptions.ShouldBe(_lbo); _result.QoSOptions.ShouldBe(_qoso); _result.HttpHandlerOptions.ShouldBe(_hho); _result.CacheOptions.ShouldBe(_co); _result.AuthenticationOptions.ShouldBe(_ao); _result.Routes.ShouldBe(_routes); _result.RequestId.ShouldBe(_fileConfig.GlobalConfiguration.RequestIdKey); _result.DownstreamScheme.ShouldBe(_fileConfig.GlobalConfiguration.DownstreamScheme); } private void ThenTheAdminPathIsSet() { _result.AdministrationPath.ShouldBe("wooty"); } private void ThenTheDepdenciesAreCalledCorrectly() { _spcCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _lboCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _qosCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _hhoCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _vCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _vpCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _mdCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Once); _vCreator.Verify(x => x.Create(It.IsAny()), Times.Once); _rlCreator.Verify(x => x.Create(It.IsAny()), Times.Once); } private void GivenTheAdminPath([CallerMemberName] string testName = nameof(ConfigurationCreatorTests)) { _adminPath = new AdministrationPath("wooty", testName); _serviceCollection.AddSingleton(_adminPath); } private void GivenTheDependenciesAreSetUp() { _fileConfig = new FileConfiguration { GlobalConfiguration = new FileGlobalConfiguration(), }; _routes = Array.Empty(); _spc = new ServiceProviderConfiguration(); _lbo = new(); _qoso = new QoSOptions(); _hho = new HttpHandlerOptions(); _co = new(new(), "region"); _ao = new(); _spcCreator.Setup(x => x.Create(It.IsAny())).Returns(_spc); _lboCreator.Setup(x => x.Create(It.IsAny())).Returns(_lbo); _qosCreator.Setup(x => x.Create(It.IsAny())).Returns(_qoso); _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); _coCreator.Setup(x => x.Create(It.IsAny())).Returns(_co); _authCreator.Setup(x => x.Create(It.IsAny())).Returns(_ao); } private void WhenICreate() { var serviceProvider = _serviceCollection.BuildServiceProvider(true); _creator = new ConfigurationCreator(serviceProvider, _authCreator.Object, _spcCreator.Object, _qosCreator.Object, _hhoCreator.Object, _lboCreator.Object, _vCreator.Object, _vpCreator.Object, _mdCreator.Object, _rlCreator.Object, _coCreator.Object); _result = _creator.Create(_fileConfig, _routes); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; [Trait("Feat", "738")] public class DefaultMetadataCreatorTests : UnitTest { private readonly DefaultMetadataCreator _sut = new(); private static readonly Dictionary Empty = new(); [Fact] public void Should_return_empty_metadata() { // Arrange, Act var result = _sut.Create(Empty, new()); // Assert result.Metadata.Keys.ShouldBeEmpty(); } [Fact] public void Should_return_global_metadata() { // Arrange var global = GivenSomeMetadataInGlobalConfiguration(); // Act var result = _sut.Create(Empty, global); // Assert ThenDownstreamMetadataMustContain(result, "foo", "bar"); } [Fact] public void Should_return_route_metadata() { // Arrange var metadata = GivenSomeMetadataInRoute(); // Act var result = _sut.Create(metadata, new()); // Assert ThenDownstreamMetadataMustContain(result, "foo", "baz"); } [Fact] public void Should_overwrite_global_metadata() { // Arrange var global = GivenSomeMetadataInGlobalConfiguration(); var metadata = GivenSomeMetadataInRoute(); // Act var result = _sut.Create(metadata, global); // Assert ThenDownstreamMetadataMustContain(result, "foo", "baz"); } private static FileGlobalConfiguration GivenSomeMetadataInGlobalConfiguration() => new() { Metadata = new Dictionary { ["foo"] = "bar", }, }; private static Dictionary GivenSomeMetadataInRoute() => new() { ["foo"] = "baz", }; private static void ThenDownstreamMetadataMustContain(MetadataOptions result, string key, string value) { result.Metadata.Keys.ShouldContain(key); result.Metadata[key].ShouldBeEquivalentTo(value); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/DownstreamAddressesCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class DownstreamAddressesCreatorTests : UnitTest { public readonly DownstreamAddressesCreator _creator; public DownstreamAddressesCreatorTests() { _creator = new DownstreamAddressesCreator(); } [Fact] public void Should_do_nothing() { // Arrange var route = new FileRoute(); var expected = new List(); // Act var result = _creator.Create(route); // Assert result.TheThenFollowingIsReturned(expected); } [Fact] public void Should_create_downstream_addresses_from_old_downstream_path_and_port() { // Arrange var route = new FileRoute { DownstreamHostAndPorts = new List { new("test", 80), }, }; var expected = new List { new("test", 80), }; // Act var result = _creator.Create(route); // Assert result.TheThenFollowingIsReturned(expected); } [Fact] public void Should_create_downstream_addresses_from_downstream_host_and_ports() { // Arrange var route = new FileRoute { DownstreamHostAndPorts = new List { new("test", 80), new("west", 443), }, }; var expected = new List { new("test", 80), new("west", 443), }; // Act var result = _creator.Create(route); // Assert result.TheThenFollowingIsReturned(expected); } } internal static class ListOfDownstreamHostAndPortExtensions { public static void TheThenFollowingIsReturned(this List actual, List expecteds) { actual.Count.ShouldBe(expecteds.Count); for (var i = 0; i < actual.Count; i++) { var result = actual[i]; var expected = expecteds[i]; result.Host.ShouldBe(expected.Host); result.Port.ShouldBe(expected.Port); } } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Metadata; using Ocelot.Values; using System.Text.Json; namespace Ocelot.UnitTests.Configuration; [Trait("Feat", "738")] public class DownstreamRouteExtensionsTests { private readonly DownstreamRoute _downstreamRoute; public DownstreamRouteExtensionsTests() { _downstreamRoute = new DownstreamRoute( null, new UpstreamPathTemplate(null, 0, false, null), new List(), new List(), new List(), null, null, new HttpHandlerOptions(), new QoSOptions(), null, null, new CacheOptions(0, null, null, null), new LoadBalancerOptions(null, null, 0), new RateLimitOptions(false), new Dictionary(), new List(), new List(), new List(), new List(), new AuthenticationOptions(null, null), new DownstreamPathTemplate(null), null, new List(), new List(), new List(), default, new SecurityOptions(), null, new Version(), HttpVersionPolicy.RequestVersionExact, new(), new MetadataOptions(new FileMetadataOptions()), 0); } [Theory] [InlineData("key1", null)] [InlineData("hello", "world")] public void Should_return_default_value_when_key_not_found(string key, string defaultValue) { // Arrange _downstreamRoute.MetadataOptions.Metadata.Add(key, defaultValue); // Act var metadataValue = _downstreamRoute.GetMetadata(key, defaultValue); // Assert metadataValue.ShouldBe(defaultValue); } [Theory] [InlineData("hello", "world")] [InlineData("object.key", "value1,value2,value3")] public void Should_return_found_metadata_value(string key, string value) { // Arrange _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(value); } [Theory] [InlineData("mykey", "")] [InlineData("mykey", "value1", "value1")] [InlineData("mykey", "value1,value2", "value1", "value2")] [InlineData("mykey", "value1, value2", "value1", "value2")] [InlineData("mykey", "value1,,,value2", "value1", "value2")] [InlineData("mykey", "value1, ,value2", "value1", "value2")] [InlineData("mykey", "value1, value2, value3", "value1", "value2", "value3")] [InlineData("mykey", ", ,value1, ,, ,,,,,value2,,, ", "value1", "value2")] public void Should_split_strings(string key, string value, params string[] expected) { // Arrange _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(expected); } [Fact] public void Should_parse_from_json_null() => Should_parse_object_from_json("mykey", "null", null); [Fact] public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "string", "string"); [Fact] public void Should_parse_from_json_numbers() => Should_parse_object_from_json("mykey", "123", 123); [Fact] public void Should_parse_from_object() => Should_parse_object_from_json( "mykey", "{\"Id\": 88, \"Value\": \"Hello World!\", \"MyTime\": \"2024-01-01T10:10:10.000Z\"}", new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) }); private void Should_parse_object_from_json(string key, string value, object expected) { // Arrange _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBeEquivalentTo(expected); } [Fact] public void Should_parse_from_json_array() { // Arrange var key = "mykey"; _downstreamRoute.MetadataOptions.Metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); // Act var metadataValue = _downstreamRoute.GetMetadata>(key); //Assert IEnumerable enumerable = metadataValue as string[] ?? metadataValue.ToArray(); enumerable.ShouldNotBeNull(); enumerable.ElementAt(0).ShouldBe("value1"); enumerable.ElementAt(1).ShouldBe("value2"); enumerable.ElementAt(2).ShouldBe("value3"); } [Fact] public void Should_throw_error_when_invalid_json() { // Arrange var key = "mykey"; _downstreamRoute.MetadataOptions.Metadata.Add(key, "[[["); // Act //Assert Assert.Throws(() => _ = _downstreamRoute.GetMetadata>(key)); } [Fact] public void Should_parse_json_with_custom_json_settings_options() { // Arrange var key = "mykey"; var value = "{\"id\": 88, \"value\": \"Hello World!\", \"myTime\": \"2024-01-01T10:10:10.000Z\"}"; var expected = new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified), }; var serializerOptions = new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var metadataValue = _downstreamRoute.GetMetadata(key, options: serializerOptions); //Assert metadataValue.ShouldBeEquivalentTo(expected); } record FakeObject { public int Id { get; set; } public string Value { get; set; } public DateTime MyTime { get; set; } } [Theory] [InlineData("0", 0)] [InlineData("99", 99)] [InlineData("500", 500)] [InlineData("999999999", 999999999)] public void Should_parse_integers(string value, int expected) => Should_parse_number(value, expected); [Theory] [InlineData("0", 0)] [InlineData("0.5", 0.5)] [InlineData("99", 99)] [InlineData("99.5", 99.5)] [InlineData("999999999", 999999999)] [InlineData("999999999.5", 999999999.5)] public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected); private void Should_parse_number(string value, T expected) { // Arrange var key = "mykey"; _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(expected); } [Fact] public void Should_throw_error_when_invalid_number() { // Arrange var key = "mykey"; _downstreamRoute.MetadataOptions.Metadata.Add(key, "xyz"); // Act // Assert Assert.Throws(() => _ = _downstreamRoute.GetMetadata(key)); } [Theory] [InlineData("true", true)] [InlineData("yes", true)] [InlineData("on", true)] [InlineData("enabled", true)] [InlineData("enable", true)] [InlineData("ok", true)] [InlineData(" true ", true)] [InlineData(" yes ", true)] [InlineData(" on ", true)] [InlineData(" enabled ", true)] [InlineData(" enable ", true)] [InlineData(" ok ", true)] [InlineData("", false)] [InlineData(" ", false)] [InlineData(null, false)] [InlineData("false", false)] [InlineData("off", false)] [InlineData("disabled", false)] [InlineData("disable", false)] [InlineData("no", false)] [InlineData("abcxyz", false)] public void Should_parse_truthy_values(string value, bool expected) { // Arrange var key = "mykey"; _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act var isTrusthy = _downstreamRoute.GetMetadata(key); //Assert isTrusthy.ShouldBe(expected); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/DownstreamRouteTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; namespace Ocelot.UnitTests.Configuration; [Collection(nameof(SequentialTests))] public class DownstreamRouteTests { [Fact] public void Name_UnknownPaths_ShouldBeQuestionMark() { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(null) .WithDownstreamPathTemplate(null) .Build(); // Act var actual = route.Name(); // Assert Assert.Equal("?", actual); } [Theory] [InlineData(null, null, "?")] [InlineData(null, "NoServiceDiscoveryDownstreamPath", "NoServiceDiscoveryDownstreamPath")] [InlineData("NoServiceDiscoveryUpstreamPath", null, "NoServiceDiscoveryUpstreamPath")] public void Name_NoServiceDiscovery_ShouldBePathTemplate(string upstreamPathTemplate, string downstreamPathTemplate, string expectedName) { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new(upstreamPathTemplate, 0, false, upstreamPathTemplate)) .WithDownstreamPathTemplate(downstreamPathTemplate) .Build(); // Act var actual = route.Name(); // Assert Assert.Equal(expectedName, actual); } [Theory] [InlineData(null, "?")] [InlineData("", "?")] [InlineData("TestTemplate", "TestTemplate")] public void Name_UpstreamPathTemplate_ShouldContainOriginalValue(string upstreamPathTemplate, string expectedName) { // Arrange var template = new UpstreamPathTemplateBuilder() .WithTemplate(upstreamPathTemplate) .WithOriginalValue(upstreamPathTemplate) .Build(); var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(template) .Build(); // Act var actual = route.Name(); // Assert Assert.Equal(expectedName, actual); } [Theory] [InlineData(null, "?")] [InlineData("", "?")] [InlineData("TestTemplate", "TestTemplate")] public void Name_DownstreamPathTemplate_ShouldContainPathTemplate(string downstreamPathTemplate, string expectedName) { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(null) .WithDownstreamPathTemplate(downstreamPathTemplate) .Build(); // Act var actual = route.Name(); // Assert Assert.Equal(expectedName, actual); } [Fact] public void Name_WithServiceDiscovery_ShouldBeUniqueDiscoveryString() { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName("TestService") .WithServiceNamespace("TestNamespace") .WithUpstreamPathTemplate(new("/UpstreamPath", 0, false, "/UpstreamPath")) .Build(); // Act var actual = route.Name(); // Assert Assert.Equal("TestNamespace:TestService:/UpstreamPath", actual); } [Theory] [InlineData(false, "/test")] [InlineData(true, "TestNamespace:TestService:/test")] public void Name_UseServiceDiscovery_ShouldContainUpstreamPathTemplate(bool useServiceDiscovery, string expectedName) { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/test", 0, false, "/test")) .WithServiceName(useServiceDiscovery ? "TestService" : string.Empty) .WithServiceNamespace("TestNamespace") .Build(); // Act var actual = route.Name(); // Assert Assert.Equal(expectedName, actual); Assert.Contains("/test", actual); } [Theory] [Trait("PR", "2073")] [InlineData(0, DownstreamRoute.DefTimeout)] // not in range [InlineData(DownstreamRoute.LowTimeout - 1, DownstreamRoute.DefTimeout)] // not in range [InlineData(DownstreamRoute.LowTimeout, DownstreamRoute.LowTimeout)] // in range [InlineData(DownstreamRoute.LowTimeout + 1, DownstreamRoute.LowTimeout + 1)] // in range [InlineData(DownstreamRoute.DefTimeout, DownstreamRoute.DefTimeout)] // in range public void DefaultTimeoutSeconds_Setter_ShouldBeGreaterThanOrEqualToThree(int value, int expected) { // Arrange, Act DownstreamRoute.DefaultTimeoutSeconds = value; // Assert Assert.Equal(expected, DownstreamRoute.DefaultTimeoutSeconds); DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; // recover clean state after assembly starting } [Fact] public void ToString_ShouldBeLoadBalancerKey() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerKey("testLbKey") .Build(); // Act var actual = route.ToString(); // Assert Assert.Equal("testLbKey", actual); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/DynamicRoutesCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class DynamicRoutesCreatorTests : UnitTest { private readonly DynamicRoutesCreator _creator; private readonly Mock _lbKeyCreator = new(); private readonly Mock _hhoCreator = new(); private readonly Mock _lboCreator = new(); private readonly Mock _qosCreator = new(); private readonly Mock _rloCreator = new(); private readonly Mock _versionCreator = new(); private readonly Mock _versionPolicyCreator = new(); private readonly Mock _metadataCreator = new(); private readonly Mock _cacheCreator = new(); private readonly Mock _authCreator = new(); private IReadOnlyList _result; private FileConfiguration _fileConfig; private RateLimitOptions[] _rlo; private Version _version; private HttpVersionPolicy _versionPolicy; private Dictionary _expectedMetadata; public DynamicRoutesCreatorTests() { _creator = new DynamicRoutesCreator( _authCreator.Object, _cacheCreator.Object, _hhoCreator.Object, _lboCreator.Object, _metadataCreator.Object, _qosCreator.Object, _rloCreator.Object, _lbKeyCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); } [Fact] public void Should_return_nothing() { // Arrange _fileConfig = new FileConfiguration(); // Act _result = _creator.Create(_fileConfig); // Assert _result.Count.ShouldBe(0); _lbKeyCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); _lboCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); _cacheCreator.Verify(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); _authCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public void Should_return_routes() { // Arrange _fileConfig = new FileConfiguration { DynamicRoutes = new() { GivenDynamicRoute("1", false, "1.1", "foo", "bar"), GivenDynamicRoute("2", true, "2.0", "foo", "baz"), }, }; GivenTheRloCreatorReturns(); GivenTheVersionCreatorReturns(); GivenTheVersionPolicyCreatorReturns(); GivenTheMetadataCreatorReturns(); // Act _result = _creator.Create(_fileConfig); // Assert ThenTheRoutesAreReturned(); ThenTheBasicCreatorsAreCalledCorrectly(); } #region PR 2073 [Fact] [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() { // Arrange var route = new FileDynamicRoute { Timeout = 11 }; var global = new FileGlobalConfiguration { Timeout = 22 }; // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Equal(route.Timeout, timeout); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() { // Arrange var route = new FileDynamicRoute(); var global = new FileGlobalConfiguration { Timeout = 22 }; // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Null(route.Timeout); Assert.Equal(global.Timeout, timeout); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() { // Arrange var route = new FileDynamicRoute(); var global = new FileGlobalConfiguration(); // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Null(route.Timeout); Assert.Null(global.Timeout); Assert.Equal(DownstreamRoute.DefTimeout, timeout); } #endregion private static FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, string downstreamHttpVersion, string key, string value) => new() { ServiceName = serviceName, RateLimitRule = new FileRateLimitByHeaderRule { EnableRateLimiting = enableRateLimiting, }, DownstreamHttpVersion = downstreamHttpVersion, Metadata = new Dictionary { [key] = value, }, }; private void ThenTheBasicCreatorsAreCalledCorrectly() { _fileConfig.DynamicRoutes.ForEach(dynamicRoute => { _authCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once); _lbKeyCreator.Verify(x => x.Create(dynamicRoute, It.IsAny()), Times.Once); _lboCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once); _rloCreator.Verify(x => x.Create(dynamicRoute, _fileConfig.GlobalConfiguration), Times.Once); _metadataCreator.Verify(x => x.Create(dynamicRoute.Metadata, _fileConfig.GlobalConfiguration), Times.Once); _versionCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersion), Times.Once); _versionPolicyCreator.Verify(x => x.Create(dynamicRoute.DownstreamHttpVersionPolicy), Times.Exactly(2)); }); } private void ThenTheRoutesAreReturned() { _result.Count.ShouldBe(2); for (int i = 0; i < _result.Count; i++) { DownstreamRoute dr = _result[i].DownstreamRoute[0]; dr.RateLimitOptions.EnableRateLimiting.ShouldBe(_rlo[i].EnableRateLimiting); dr.RateLimitOptions.ShouldBe(_rlo[i]); dr.DownstreamHttpVersion.ShouldBe(_version); dr.DownstreamHttpVersionPolicy.ShouldBe(_versionPolicy); dr.ServiceName.ShouldBe(_fileConfig.DynamicRoutes[i].ServiceName); } } private void GivenTheVersionCreatorReturns() { _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); } private void GivenTheVersionPolicyCreatorReturns() { _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); } private void GivenTheMetadataCreatorReturns() { _expectedMetadata = new() { ["foo"] = "bar", }; _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) .Returns(new MetadataOptions() { Metadata = _expectedMetadata }); } private void GivenTheRloCreatorReturns() { _rlo = [ new() { EnableRateLimiting = false }, new() { EnableRateLimiting = true }, ]; _rloCreator .SetupSequence(x => x.Create(It.IsAny(), It.IsAny())) .Returns(_rlo[0]) .Returns(_rlo[1]); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileConfigurationSetterTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration; public class FileConfigurationSetterTests : UnitTest { private FileConfiguration _fileConfiguration; private readonly FileAndInternalConfigurationSetter _configSetter; private readonly Mock _configRepo; private readonly Mock _configCreator; private Response _configuration; private object _result; private readonly Mock _repo; public FileConfigurationSetterTests() { _repo = new Mock(); _configRepo = new Mock(); _configCreator = new Mock(); _configSetter = new FileAndInternalConfigurationSetter(_configRepo.Object, _configCreator.Object, _repo.Object); } [Fact] public async Task Should_set_configuration() { // Arrange _fileConfiguration = new FileConfiguration(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); var config = new InternalConfiguration() { AdministrationPath = string.Empty, ServiceProviderConfiguration = serviceProviderConfig, RequestId = "asdf", LoadBalancerOptions = new(), DownstreamScheme = string.Empty, QoSOptions = new(), HttpHandlerOptions = new(), DownstreamHttpVersion = new Version("1.1"), DownstreamHttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower, MetadataOptions = new(), RateLimitOptions = new(), Timeout = 111, }; GivenTheRepoReturns(new OkResponse()); GivenTheCreatorReturns(new OkResponse(config)); // Act _result = await _configSetter.Set(_fileConfiguration); // Assert ThenTheConfigurationRepositoryIsCalledCorrectly(); } [Fact] public async Task Should_return_error_if_unable_to_set_file_configuration() { // Arrange _fileConfiguration = new FileConfiguration(); GivenTheRepoReturns(new ErrorResponse(It.IsAny())); // Act _result = await _configSetter.Set(_fileConfiguration); // Assert _result.ShouldBeOfType(); } [Fact] public async Task Should_return_error_if_unable_to_set_ocelot_configuration() { // Arrange _fileConfiguration = new FileConfiguration(); GivenTheRepoReturns(new OkResponse()); GivenTheCreatorReturns(new ErrorResponse(It.IsAny())); // Act _result = await _configSetter.Set(_fileConfiguration); // Assert _result.ShouldBeOfType(); } private void GivenTheRepoReturns(Response response) { _repo .Setup(x => x.Set(It.IsAny())) .ReturnsAsync(response); } private void GivenTheCreatorReturns(Response configuration) { _configuration = configuration; _configCreator .Setup(x => x.Create(_fileConfiguration)) .ReturnsAsync(_configuration); } private void ThenTheConfigurationRepositoryIsCalledCorrectly() { _configRepo.Verify(x => x.AddOrReplace(_configuration.Data), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileInternalConfigurationCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using Ocelot.Errors; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Configuration; public class FileInternalConfigurationCreatorTests : UnitTest { private readonly Mock _validator; private readonly Mock _routesCreator; private readonly Mock _aggregatesCreator; private readonly Mock _dynamicsCreator; private readonly Mock _configCreator; private FileConfiguration _fileConfiguration; private readonly FileInternalConfigurationCreator _creator; private Response _result; private List _routes; private List _aggregates; private List _dynamics; private InternalConfiguration _internalConfig; public FileInternalConfigurationCreatorTests() { _validator = new Mock(); _routesCreator = new Mock(); _aggregatesCreator = new Mock(); _dynamicsCreator = new Mock(); _configCreator = new Mock(); _creator = new FileInternalConfigurationCreator(_validator.Object, _routesCreator.Object, _aggregatesCreator.Object, _dynamicsCreator.Object, _configCreator.Object); } [Fact] public async Task Should_return_validation_error() { // Arrange _fileConfiguration = new FileConfiguration(); GivenTheValidationFails(); // Act _result = await _creator.Create(_fileConfiguration); // Assert _result.IsError.ShouldBeTrue(); } [Fact] public async Task Should_return_internal_configuration() { // Arrange _fileConfiguration = new FileConfiguration(); GivenTheValidationSucceeds(); GivenTheDependenciesAreSetUp(); // Act _result = await _creator.Create(_fileConfiguration); // Assert ThenTheDependenciesAreCalledCorrectly(); } private void ThenTheDependenciesAreCalledCorrectly() { _routesCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); _aggregatesCreator.Verify(x => x.Create(_fileConfiguration, _routes), Times.Once); _dynamicsCreator.Verify(x => x.Create(_fileConfiguration), Times.Once); var mergedRoutes = _routes .Union(_aggregates) .Union(_dynamics) .ToArray(); _configCreator.Verify(x => x.Create(_fileConfiguration, It.Is(y => y.Length == mergedRoutes.Length)), Times.Once); } private void GivenTheDependenciesAreSetUp() { _routes = new List { new() }; _aggregates = new List { new() }; _dynamics = new List { new() }; _internalConfig = new InternalConfiguration(); _routesCreator.Setup(x => x.Create(It.IsAny())).Returns(_routes); _aggregatesCreator.Setup(x => x.Create(It.IsAny(), It.IsAny>())).Returns(_aggregates); _dynamicsCreator.Setup(x => x.Create(It.IsAny())).Returns(_dynamics); _configCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_internalConfig); } private void GivenTheValidationSucceeds() { var ok = new ConfigurationValidationResult(false); var response = new OkResponse(ok); _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); } private void GivenTheValidationFails() { var error = new ConfigurationValidationResult(true, new List { new AnyError() }); var response = new OkResponse(error); _validator.Setup(x => x.IsValid(It.IsAny())).ReturnsAsync(response); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileModels/FileDynamicRouteTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration.FileModels; public class FileDynamicRouteTests { [Fact] public void Ctor() { // Arrange, Act FileDynamicRoute instance = new(); // Assert Assert.Null(instance.Metadata); Assert.Null(instance.Key); Assert.Null(instance.RateLimitRule); Assert.Null(instance.RateLimitOptions); } [Fact] public void Ctor_IRouteGrouping_IsImplemented() { // Arrange, Act FileDynamicRoute instance = new() { Key = "abc" }; // Assert Assert.IsAssignableFrom(instance); IRouteGrouping obj = instance; Assert.Equal("abc", obj.Key); } [Fact] public void Ctor_IRouteRateLimiting_IsImplemented() { // Arrange FileRateLimitByHeaderRule rule = new() { ClientIdHeader = "111" }; // Act FileDynamicRoute instance = new() { RateLimitOptions = rule }; // Assert Assert.IsAssignableFrom(instance); IRouteRateLimiting obj = instance; Assert.Equal(rule, obj.RateLimitOptions); Assert.Equal("111", obj.RateLimitOptions.ClientIdHeader); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileModels/FileGlobalHttpHandlerOptionsTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration.FileModels; [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "2320")] // https://github.com/ThreeMammals/Ocelot/issues/2320 public class FileGlobalHttpHandlerOptionsTests { [Fact] public void Ctor_Default() { // Arrange, Act FileGlobalHttpHandlerOptions actual = new(); // Assert Assert.Null(actual.RouteKeys); Assert.Null(actual.UseTracing); } [Fact] public void Ctor_FileHttpHandlerOptions() { // Arrange FileHttpHandlerOptions from = new() { AllowAutoRedirect = true, MaxConnectionsPerServer = 111, PooledConnectionLifetimeSeconds = 222, UseCookieContainer = true, UseProxy = true, UseTracing = true, }; // Act FileGlobalHttpHandlerOptions actual = new(from); // Assert Assert.Null(actual.RouteKeys); Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); Assert.True(actual.AllowAutoRedirect); Assert.Equal(111, actual.MaxConnectionsPerServer); Assert.Equal(222, actual.PooledConnectionLifetimeSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileModels/FileMetadataOptionsTests.cs ================================================ using Ocelot.Configuration.File; using System.Globalization; namespace Ocelot.UnitTests.Configuration.FileModels; public class FileMetadataOptionsTests { [Fact] public void Ctor_Default() { // Arrange, Act FileMetadataOptions actual = new(); // Assert Assert.Contains(",", actual.Separators); Assert.Contains(" ", actual.TrimChars); } [Fact] public void Ctor_CopyingFrom() { // Arrange FileMetadataOptions from = new() { CurrentCulture = CultureInfo.GetCultureInfo("uk").Name, NumberStyle = NumberStyles.None.ToString(), Separators = ["|"], StringSplitOption = StringSplitOptions.TrimEntries.ToString(), TrimChars = ['x'], }; // Act FileMetadataOptions actual = new(from); // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); Assert.Equal("uk", actual.CurrentCulture); Assert.Equal("None", actual.NumberStyle); Assert.Contains("|", actual.Separators); Assert.Equal("TrimEntries", actual.StringSplitOption); Assert.Contains('x', actual.TrimChars); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/FileModels/FileRouteTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration.FileModels; public class FileRouteTests : UnitTest { [Fact] [Trait("PR", "1753")] public void Ctor_Copying_Copied() { // Arrange var expected = GivenFileRoute(); // Act FileRoute actual = new(expected); // copying // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } [Fact] [Trait("PR", "1753")] public void Clone_ShouldClone() { // Arrange var expected = GivenFileRoute(); // Act var obj = expected.Clone(); var actual = Assert.IsType(obj); // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void ToString_NoKeyNoDiscovery_ShouldBeUpstreamPathTemplate() { // Arrange var route = GivenFileRoute(); route.Key = null; route.ServiceName = null; route.UpstreamPathTemplate = "/upstream"; // Act var actual = route.ToString(); // Assert Assert.Equal("/upstream", actual); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, "?")] [InlineData("/downstream", "/downstream")] public void ToString_NoKeyNoUpstreamPathTemplate_ShouldBeDownstreamPathTemplate(string downstream, string expected) { // Arrange var route = GivenFileRoute(); route.Key = null; route.ServiceName = null; route.UpstreamPathTemplate = null; route.DownstreamPathTemplate = downstream; // Act var actual = route.ToString(); // Assert Assert.Equal(expected, actual); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void ToString_NoKeyWithServiceDiscovery_ShouldContainServiceName() { // Arrange var route = GivenFileRoute(); route.Key = null; route.ServiceName = TestName(); route.UpstreamPathTemplate = "/upstream"; // Act var actual = route.ToString(); // Assert Assert.True(actual.Contains(route.ServiceName)); Assert.Equal("test-namespace:ToString_NoKeyWithServiceDiscovery_ShouldContainServiceName:/upstream", actual); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void ToString_HasKey_ShouldBeKey() { // Arrange var route = GivenFileRoute(); route.Key = TestName(); // Act var actual = route.ToString(); // Assert Assert.Equal(nameof(ToString_HasKey_ShouldBeKey), actual); } private static FileRoute GivenFileRoute() { FileRoute expected = new(); expected.AddClaimsToRequest.Add("key1", "value1"); expected.AddHeadersToRequest.Add("key2", "value2"); expected.AddQueriesToRequest.Add("key3", "value3"); expected.AuthenticationOptions = new("value4"); expected.ChangeDownstreamPathTemplate.Add("key5", "value5"); expected.DangerousAcceptAnyServerCertificateValidator = true; expected.DelegatingHandlers.Add("value6"); expected.DownstreamHeaderTransform.Add("key7", "value7"); expected.DownstreamHostAndPorts.Add(new("host8", 8)); expected.DownstreamHttpMethod = "value9"; expected.DownstreamHttpVersion = "value10"; expected.DownstreamHttpVersionPolicy = "value11"; expected.DownstreamPathTemplate = "value12"; expected.DownstreamScheme = "value13"; expected.CacheOptions = new() { Header = "value14" }; expected.FileCacheOptions = new() { TtlSeconds = 14 }; expected.HttpHandlerOptions = new() { MaxConnectionsPerServer = 15 }; expected.Key = "value16"; expected.LoadBalancerOptions ??= new("value17"); expected.Metadata ??= new Dictionary() { { "key18", "value18" } }; expected.Priority = 19; expected.QoSOptions = new() { DurationOfBreak = 20 }; expected.RateLimitOptions ??= new() { Period = "value21" }; expected.RequestIdKey = "value22"; expected.RouteClaimsRequirement.Add("key23", "value23"); expected.RouteIsCaseSensitive = true; expected.SecurityOptions.IPAllowedList.Add("value24"); expected.ServiceName = "test-service"; expected.ServiceNamespace = "test-namespace"; expected.Timeout = 27; expected.UpstreamHeaderTemplates.Add("key28", "value28"); expected.UpstreamHeaderTransform.Add("key29", "value29"); expected.UpstreamHost = "value30"; expected.UpstreamHttpMethod.Add("value31"); expected.UpstreamPathTemplate = "value32"; return expected; } private static void AssertEquality(FileRoute actual, FileRoute expected) { Assert.Equal(expected.AddClaimsToRequest, actual.AddClaimsToRequest); Assert.Equal(expected.AddHeadersToRequest, actual.AddHeadersToRequest); Assert.Equal(expected.AddQueriesToRequest, actual.AddQueriesToRequest); Assert.Equivalent(expected.AuthenticationOptions, actual.AuthenticationOptions); // FileAuthenticationOptions requires Equals overriding Assert.Equal(expected.ChangeDownstreamPathTemplate, actual.ChangeDownstreamPathTemplate); Assert.Equal(expected.DangerousAcceptAnyServerCertificateValidator, actual.DangerousAcceptAnyServerCertificateValidator); Assert.Equal(expected.DelegatingHandlers, actual.DelegatingHandlers); Assert.Equal(expected.DownstreamHeaderTransform, actual.DownstreamHeaderTransform); Assert.Equivalent(expected.DownstreamHostAndPorts, actual.DownstreamHostAndPorts); // FileHostAndPort requires Equals overriding Assert.Equal(expected.DownstreamHttpMethod, actual.DownstreamHttpMethod); Assert.Equal(expected.DownstreamHttpVersion, actual.DownstreamHttpVersion); Assert.Equal(expected.DownstreamHttpVersionPolicy, actual.DownstreamHttpVersionPolicy); Assert.Equal(expected.DownstreamPathTemplate, actual.DownstreamPathTemplate); Assert.Equal(expected.DownstreamScheme, actual.DownstreamScheme); Assert.Equivalent(expected.CacheOptions, actual.CacheOptions); // FileCacheOptions requires Equals overriding Assert.Equivalent(expected.FileCacheOptions, actual.FileCacheOptions); // FileCacheOptions requires Equals overriding Assert.Equivalent(expected.HttpHandlerOptions, actual.HttpHandlerOptions); // FileHttpHandlerOptions requires Equals overriding Assert.Equal(expected.Key, actual.Key); Assert.Equivalent(expected.LoadBalancerOptions, actual.LoadBalancerOptions); // FileLoadBalancerOptions requires Equals overriding Assert.Equal(expected.Metadata, actual.Metadata); Assert.Equal(expected.Priority, actual.Priority); Assert.Equivalent(expected.QoSOptions, actual.QoSOptions); // FileQoSOptions requires Equals overriding Assert.Equivalent(expected.RateLimitOptions, actual.RateLimitOptions); // FileRateLimitByHeaderRule requires Equals overriding Assert.Equal(expected.RequestIdKey, actual.RequestIdKey); Assert.Equal(expected.RouteClaimsRequirement, actual.RouteClaimsRequirement); Assert.Equal(expected.RouteIsCaseSensitive, actual.RouteIsCaseSensitive); Assert.Equivalent(expected.SecurityOptions, actual.SecurityOptions); // FileSecurityOptions requires Equals overriding Assert.Equal(expected.ServiceName, actual.ServiceName); Assert.Equal(expected.ServiceNamespace, actual.ServiceNamespace); Assert.Equal(expected.Timeout, actual.Timeout); Assert.Equal(expected.UpstreamHeaderTemplates, actual.UpstreamHeaderTemplates); Assert.Equal(expected.UpstreamHeaderTransform, actual.UpstreamHeaderTransform); Assert.Equal(expected.UpstreamHost, actual.UpstreamHost); Assert.Equal(expected.UpstreamHttpMethod, actual.UpstreamHttpMethod); Assert.Equal(expected.UpstreamPathTemplate, actual.UpstreamPathTemplate); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HashCreationTests.cs ================================================ using Microsoft.AspNetCore.Cryptography.KeyDerivation; using System.Security.Cryptography; namespace Ocelot.UnitTests.Configuration; public class HashCreationTests { [Fact] public void Should_create_hash_and_salt() { var password = "secret"; var salt = new byte[128 / 8]; using var rng = RandomNumberGenerator.Create(); rng.GetBytes(salt); var storedSalt = Convert.ToBase64String(salt); var storedHash = Convert.ToBase64String(KeyDerivation.Pbkdf2( password: password, salt: salt, prf: KeyDerivationPrf.HMACSHA256, iterationCount: 10000, numBytesRequested: 256 / 8)); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs ================================================ using Microsoft.Extensions.Options; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Configuration; public class HeaderFindAndReplaceCreatorTests : UnitTest { private readonly HeaderFindAndReplaceCreator _creator; private readonly FileGlobalConfiguration _global; private HeaderTransformations _result; private readonly Mock _placeholders; private readonly Mock _factory; private readonly Mock _logger; private readonly List> _messages = new(); public HeaderFindAndReplaceCreatorTests() { _logger = new Mock(); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(_messages.Add); _factory = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _placeholders = new Mock(); _global = new FileGlobalConfiguration(); _global.UpstreamHeaderTransform.Add("TestGlobal", "Test, Chicken"); _global.UpstreamHeaderTransform.Add("MoopGlobal", "o, a"); _global.DownstreamHeaderTransform.Add("PopGlobal", "West, East"); _global.DownstreamHeaderTransform.Add("BopGlobal", "e, r"); var options = new Mock>(); options.Setup(x => x.Value).Returns(_global); _creator = new HeaderFindAndReplaceCreator(_placeholders.Object, _factory.Object, options.Object); } [Fact] [Trait("Feat", "204")] // https://github.com/ThreeMammals/Ocelot/pull/204 [Trait("Feat", "1658")] // https://github.com/ThreeMammals/Ocelot/issues/1658 [Trait("PR", "1659")] // https://github.com/ThreeMammals/Ocelot/pull/1659 public void Should_create() { // Arrange var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {"Test", "Test, Chicken"}, {"Moop", "o, a"}, }, DownstreamHeaderTransform = new Dictionary { {"Pop", "West, East"}, {"Bop", "e, r"}, }, }; var upstream = new List { new("Test", "Test", "Chicken", 0), new("Moop", "o", "a", 0), new("TestGlobal", "Test", "Chicken", 0), new("MoopGlobal", "o", "a", 0), }; var downstream = new List { new("Pop", "West", "East", 0), new("Bop", "e", "r", 0), new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; // Act _result = _creator.Create(route); // Assert ThenTheFollowingUpstreamIsReturned(upstream); ThenTheFollowingDownstreamIsReturned(downstream); } [Fact] [Trait("Feat", "1658")] public void Create_WithRouteAndWithoutGlobalConfigurationParam_GlobalConfigurationInjectionIsReused() { // Arrange var route = new FileRoute(); // no data var upstream = new List { new(_global.UpstreamHeaderTransform.First()), new(_global.UpstreamHeaderTransform.Last()), }; var downstream = new List { new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; // Act _result = _creator.Create(route, null); // Assert ThenTheFollowingUpstreamIsReturned(upstream); ThenTheFollowingDownstreamIsReturned(downstream); } [Fact] [Trait("Feat", "623")] // https://github.com/ThreeMammals/Ocelot/issues/623 [Trait("PR", "632")] // https://github.com/ThreeMammals/Ocelot/pull/632 public void Should_create_with_add_headers_to_request() { // Arrange const string key = "X-Forwarded-For"; const string value = "{RemoteIpAddress}"; var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {key, value}, }, }; var expected = new AddHeader(key, value); // Act _result = _creator.Create(route); // Assert ThenTheFollowingAddHeaderToUpstreamIsReturned(expected); } [Fact] public void Should_use_base_url_placeholder() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, }, }; var downstream = new List { new("Location", "http://www.bbc.co.uk/", "http://ocelot.com/", 0), new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; GivenThePlaceholderIs("http://ocelot.com/"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingDownstreamIsReturned(downstream); } [Fact] [Trait("Feat", "204")] [Trait("Feat", "1658")] public void Should_log_errors_and_not_add_headers() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, }, UpstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/, {BaseUrl}"}, }, }; var expectedDownstream = new List { new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; var expectedUpstream = new List { new("TestGlobal", "Test", "Chicken", 0), new("MoopGlobal", "o", "a", 0), }; GivenTheBaseUrlErrors(); // Act _result = _creator.Create(route); // Assert ThenTheFollowingDownstreamIsReturned(expectedDownstream); ThenTheFollowingUpstreamIsReturned(expectedUpstream); ThenTheLoggerIsCalledCorrectly(4, "HeaderFindAndReplace was not mapped from [Location, http://www.bbc.co.uk/, {BaseUrl}] due to UnknownError: blahh", "Unable to add UpstreamHeaderTransform [Location, http://www.bbc.co.uk/, {BaseUrl}]", "HeaderFindAndReplace was not mapped from [Location, http://www.bbc.co.uk/, {BaseUrl}] due to UnknownError: blahh", "Unable to add DownstreamHeaderTransform [Location, http://www.bbc.co.uk/, {BaseUrl}]"); } private void ThenTheLoggerIsCalledCorrectly(int times, params string[] messages) { _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Exactly(times)); _messages.ShouldNotBeEmpty(); var actual = _messages.Select(f => f.Invoke()).ToList(); foreach (var expected in messages) { actual.ShouldContain(expected); } } [Fact] public void Should_use_base_url_partial_placeholder() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"Location", "http://www.bbc.co.uk/pay, {BaseUrl}pay"}, }, }; var downstream = new List { new("Location", "http://www.bbc.co.uk/pay", "http://ocelot.com/pay", 0), new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; GivenThePlaceholderIs("http://ocelot.com/"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingDownstreamIsReturned(downstream); } [Fact] [Trait("Feat", "204")] public void Should_map_with_partial_placeholder_in_the_middle() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"Host-Next", "www.bbc.co.uk, subdomain.{Host}/path"}, }, }; var expected = new List { new("Host-Next", "www.bbc.co.uk", "subdomain.ocelot.next/path", 0), new(_global.DownstreamHeaderTransform.First()), new(_global.DownstreamHeaderTransform.Last()), }; GivenThePlaceholderIs("ocelot.next"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingDownstreamIsReturned(expected); } [Fact] public void Should_add_trace_id_header() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"Trace-Id", "{TraceId}"}, }, }; var expected = new AddHeader("Trace-Id", "{TraceId}"); GivenThePlaceholderIs("http://ocelot.com/"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingAddHeaderToDownstreamIsReturned(expected); } [Fact] public void Should_add_downstream_header_as_is_when_no_replacement_is_given() { // Arrange var route = new FileRoute { DownstreamHeaderTransform = new Dictionary { {"X-Custom-Header", "Value"}, }, }; var expected = new AddHeader("X-Custom-Header", "Value"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingAddHeaderToDownstreamIsReturned(expected); } [Fact] public void Should_add_upstream_header_as_is_when_no_replacement_is_given() { // Arrange var route = new FileRoute { UpstreamHeaderTransform = new Dictionary { {"X-Custom-Header", "Value"}, }, }; var expected = new AddHeader("X-Custom-Header", "Value"); // Act _result = _creator.Create(route); // Assert ThenTheFollowingAddHeaderToUpstreamIsReturned(expected); } [Fact] [Trait("PR", "1659")] [Trait("Feat", "1658")] public void Merge_ShouldMergeGlobalIntoRouteOpts() { // Arrange var routeTransforms = new Dictionary() { { "B", "routeB" }, { "C", "routeC" }, }; var globalTransforms = new Dictionary() { { "A", "globalA" }, { "B", "globalB" }, }; // Act var actual = HeaderFindAndReplaceCreator.Merge(routeTransforms, globalTransforms); // Assert actual.ShouldNotBeNull(); var dictionary = actual.ToDictionary(x => x.Key, x => x.Value); dictionary.Count.ShouldBe(3); dictionary.ContainsKey("A").ShouldBeTrue(); dictionary["A"].ShouldBe("globalA"); dictionary.ContainsKey("B").ShouldBeTrue(); dictionary["B"].ShouldBe("routeB"); // local value wins over global one dictionary.ContainsKey("C").ShouldBeTrue(); dictionary["C"].ShouldBe("routeC"); } [Fact] [Trait("PR", "1659")] [Trait("Feat", "1658")] public void Merge_NullParams_NullChecksHaveBeenPerformed() { // Arrange, Act var actual = HeaderFindAndReplaceCreator.Merge(null, null); // Assert actual.ShouldNotBeNull().ShouldBeEmpty(); } private void GivenThePlaceholderIs(string placeholderValue) { _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(placeholderValue)); } private void GivenTheBaseUrlErrors() { _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); } private void ThenTheFollowingAddHeaderToDownstreamIsReturned(AddHeader addHeader) { _result.AddHeadersToDownstream[0].Key.ShouldBe(addHeader.Key); _result.AddHeadersToDownstream[0].Value.ShouldBe(addHeader.Value); } private void ThenTheFollowingAddHeaderToUpstreamIsReturned(AddHeader addHeader) { _result.AddHeadersToUpstream[0].Key.ShouldBe(addHeader.Key); _result.AddHeadersToUpstream[0].Value.ShouldBe(addHeader.Value); } private void ThenTheFollowingDownstreamIsReturned(List downstream) { _result.Downstream.Count.ShouldBe(downstream.Count); for (var i = 0; i < _result.Downstream.Count; i++) { var result = _result.Downstream[i]; var expected = downstream[i]; result.Find.ShouldBe(expected.Find); result.Index.ShouldBe(expected.Index); result.Key.ShouldBe(expected.Key); result.Replace.ShouldBe(expected.Replace); } } private void ThenTheFollowingUpstreamIsReturned(List expecteds) { _result.Upstream.Count.ShouldBe(expecteds.Count); for (var i = 0; i < _result.Upstream.Count; i++) { var result = _result.Upstream[i]; var expected = expecteds[i]; result.Find.ShouldBe(expected.Find); result.Index.ShouldBe(expected.Index); result.Key.ShouldBe(expected.Key); result.Replace.ShouldBe(expected.Replace); } } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceTests.cs ================================================ using Ocelot.Configuration; namespace Ocelot.UnitTests.Configuration; public class HeaderFindAndReplaceTests { [Fact] [Trait("PR", "1659")] public void Ctor_Copying_Copied() { // Arrange HeaderFindAndReplace expected = new("1", "2", "3", 4); // Act HeaderFindAndReplace actual = new(expected); // copying // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } [Fact] [Trait("PR", "1659")] public void Ctor_KeyValuePair_Copied() { // Arrange KeyValuePair from = new("Location", "XXX, YYY"); HeaderFindAndReplace expected = new("Location", "XXX", "YYY", 0); // Act HeaderFindAndReplace actual = new(from); // copying // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } private const string Em = ""; [Theory] [Trait("PR", "1659")] [InlineData(null, Em, Em)] [InlineData("", Em, Em)] [InlineData(" ", Em, Em)] [InlineData("x", "x", Em)] [InlineData("x,y", "x", "y")] public void Ctor_KeyValuePair_ArgIsChecked(string value, string find, string replace) { // Arrange KeyValuePair from = new("key", value); // Act HeaderFindAndReplace actual = new(from); // Assert Assert.Equal(0, actual.Index); Assert.Equal("key", actual.Key); Assert.Equal(find, actual.Find); Assert.Equal(replace, actual.Replace); } [Fact] [Trait("PR", "1659")] public void ToString_Serialized() { // Arrange HeaderFindAndReplace headerFR = new("Location", "XXX", "YYY", 3); // Act var actual = headerFR.ToString(); // Assert Assert.Equal("HeaderFindAndReplace[Location at 3: XXX -> YYY]", actual); } private static void AssertEquality(HeaderFindAndReplace actual, HeaderFindAndReplace expected) { Assert.Equal(expected.Index, actual.Index); Assert.Equal(expected.Key, actual.Key); Assert.Equal(expected.Find, actual.Find); Assert.Equal(expected.Replace, actual.Replace); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsCreatorTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Logging; using System.Reflection; namespace Ocelot.UnitTests.Configuration; public class HttpHandlerOptionsCreatorTests : UnitTest { private HttpHandlerOptionsCreator _creator; private readonly Mock _tracer = new(); public HttpHandlerOptionsCreatorTests() { Arrange(); } private void Arrange(bool hasTracer = true) { var services = new ServiceCollection(); if (hasTracer) services.AddSingleton(_tracer.Object); var provider = services.BuildServiceProvider(true); _creator = new HttpHandlerOptionsCreator(provider); } [Fact] public void Ctor() { // Act Arrange(); // Assert var field = _creator.GetType().GetField(nameof(_tracer), BindingFlags.Instance | BindingFlags.NonPublic); Assert.NotNull(field); var tracer = field.GetValue(_creator) as IOcelotTracer; Assert.NotNull(tracer); } [Theory] [InlineData(true, true, false)] [InlineData(false, false, false)] [InlineData(false, true, true)] public void Create_FileHttpHandlerOptions(bool isNull, bool hasTracer, bool expectedUseTracing) { Arrange(hasTracer); FileHttpHandlerOptions opts = isNull ? null : new() { UseTracing = true, }; // Act var actual = _creator.Create(opts); // Assert Assert.Equal(expectedUseTracing, actual.UseTracing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void Create_FromRoute_NullChecks() { // Arrange, Act, Assert FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void Create_FromRoute() { // Arrange var opts = RouteOptions(); opts.AllowAutoRedirect = null; opts.MaxConnectionsPerServer = null; var route = GivenRoute(opts); // Act var actual = _creator.Create(route, GlobalConfiguration()); // Assert Assert.False(actual.AllowAutoRedirect); Assert.Equal(111, actual.MaxConnectionsPerServer); Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void Create_FromDynamicRoute_NullChecks() { // Arrange, Act, Assert FileDynamicRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void Create_FromDynamicRoute() { // Arrange var opts = RouteOptions(); opts.AllowAutoRedirect = null; opts.MaxConnectionsPerServer = null; var route = GivenDynamicRoute(opts); // Act var actual = _creator.Create(route, GlobalConfiguration()); // Assert Assert.False(actual.AllowAutoRedirect); Assert.Equal(111, actual.MaxConnectionsPerServer); Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void CreateProtected_NullCheck() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); IRouteGrouping grouping = null; FileHttpHandlerOptions options = null; FileGlobalHttpHandlerOptions globalOptions = null; // Act var wrapper = Assert.Throws( () => method.Invoke(_creator, [grouping, options, globalOptions])); // Assert Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(grouping), actual.ParamName); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void CreateProtected() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new() { Key = "r1" }; FileHttpHandlerOptions options = null; var configuration = GlobalConfiguration(); FileGlobalHttpHandlerOptions globalOptions = configuration.HttpHandlerOptions; // Act, Assert var actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.False(actual.AllowAutoRedirect); // global Assert.Equal(111, actual.MaxConnectionsPerServer); Assert.Equal(111, actual.PooledConnectionLifeTime.TotalSeconds); Assert.False(actual.UseCookieContainer); Assert.False(actual.UseProxy); Assert.False(actual.UseTracing); // Arrange 2 options = RouteOptions(); globalOptions.RouteKeys = ["?"]; // Act, Assert 2 actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.True(actual.AllowAutoRedirect); // route Assert.Equal(333, actual.MaxConnectionsPerServer); Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); globalOptions.RouteKeys = ["r1"]; actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.True(actual.AllowAutoRedirect); // route Assert.Equal(333, actual.MaxConnectionsPerServer); Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); globalOptions = null; actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.True(actual.AllowAutoRedirect); // route Assert.Equal(333, actual.MaxConnectionsPerServer); Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); // Arrange 3 options.MaxConnectionsPerServer = null; // -> global globalOptions = configuration.HttpHandlerOptions; globalOptions.RouteKeys = null; actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal(111, actual.MaxConnectionsPerServer); // global Assert.True(actual.AllowAutoRedirect); // route Assert.Equal(333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.True(actual.UseCookieContainer); Assert.True(actual.UseProxy); Assert.True(actual.UseTracing); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void CreateProtected_NoOptions() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new(); FileHttpHandlerOptions options = null; FileGlobalHttpHandlerOptions globalOptions = null; // Act var actual = (HttpHandlerOptions)method.Invoke(_creator, [route, options, globalOptions]); // Assert : parameterless constructor was called Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer); Assert.Equal(HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds, actual.PooledConnectionLifeTime.TotalSeconds); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 public void Merge_NullCheck() { // Arrange var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); FileHttpHandlerOptions options = null; FileHttpHandlerOptions globalOptions = null; // Act, Assert 1 var wrapper = Assert.Throws( () => method.Invoke(_creator, [null, globalOptions])); Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(options), actual.ParamName); // Act, Assert 2 options = new(); wrapper = Assert.Throws( () => method.Invoke(_creator, [options, null])); Assert.IsType(wrapper.InnerException); actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(globalOptions), actual.ParamName); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "2320")] [Trait("PR", "2332")] // https://github.com/ThreeMammals/Ocelot/pull/2332 [InlineData(false, true)] [InlineData(true, true)] [InlineData(false, false)] [InlineData(true, false)] public void Merge(bool isDef, bool hasTracer) { // Arrange Arrange(hasTracer); var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); FileHttpHandlerOptions options = new() { AllowAutoRedirect = isDef ? null : true, MaxConnectionsPerServer = isDef ? null : 333, PooledConnectionLifetimeSeconds = isDef ? null : 333, UseCookieContainer = isDef ? null : true, UseProxy = isDef ? null : true, UseTracing = isDef ? null : true, }; FileHttpHandlerOptions globalOptions = new() { AllowAutoRedirect = isDef ? null : false, MaxConnectionsPerServer = isDef ? null : 111, PooledConnectionLifetimeSeconds = isDef ? null : 111, UseCookieContainer = isDef ? null : false, UseProxy = isDef ? null : false, UseTracing = isDef ? null : false, }; // Act var actual = (HttpHandlerOptions)method.Invoke(_creator, [options, globalOptions]); // Assert Assert.Equal(!isDef, actual.AllowAutoRedirect); Assert.Equal(isDef ? int.MaxValue : 333, actual.MaxConnectionsPerServer); Assert.Equal(isDef ? HttpHandlerOptions.DefaultPooledConnectionLifetimeSeconds : 333, actual.PooledConnectionLifeTime.TotalSeconds); Assert.Equal(!isDef, actual.UseCookieContainer); Assert.Equal(!isDef, actual.UseProxy); Assert.Equal(hasTracer && !isDef, actual.UseTracing); // the useTracing parameter takes absolute priority } private static FileHttpHandlerOptions RouteOptions() => new() { AllowAutoRedirect = true, MaxConnectionsPerServer = 333, PooledConnectionLifetimeSeconds = 333, UseCookieContainer = true, UseProxy = true, UseTracing = true, }; private static FileGlobalConfiguration GlobalConfiguration() => new() { HttpHandlerOptions = new() { RouteKeys = null, AllowAutoRedirect = false, MaxConnectionsPerServer = 111, PooledConnectionLifetimeSeconds = 111, UseCookieContainer = false, UseProxy = false, UseTracing = false, }, }; private static FileRoute GivenRoute(FileHttpHandlerOptions options = null) => new() { HttpHandlerOptions = options ?? new() }; private static FileDynamicRoute GivenDynamicRoute(FileHttpHandlerOptions options = null) => new() { HttpHandlerOptions = options ?? new() }; } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HttpHandlerOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class HttpHandlerOptionsTests { [Fact] public void Ctor() { // Arrange, Act HttpHandlerOptions actual = new(); // Assert Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer); Assert.Equal(120, actual.PooledConnectionLifeTime.TotalSeconds); } [Theory] [InlineData(false)] [InlineData(true)] public void Ctor_FileHttpHandlerOptions(bool isNull) { // Arrange bool? nullBool() => isNull ? null : true; int? nullInt() => isNull ? null : 123; FileHttpHandlerOptions from = new() { AllowAutoRedirect = nullBool(), MaxConnectionsPerServer = nullInt(), PooledConnectionLifetimeSeconds = nullInt(), UseCookieContainer = nullBool(), UseProxy = nullBool(), UseTracing = nullBool(), }; // Act HttpHandlerOptions actual = new(from); // Assert bool expectedBool = !isNull; Assert.Equal(expectedBool, actual.AllowAutoRedirect); Assert.Equal(isNull ? int.MaxValue : 123, actual.MaxConnectionsPerServer); Assert.Equal(isNull ? 120 : 123, (int)actual.PooledConnectionLifeTime.TotalSeconds); Assert.Equal(expectedBool, actual.UseCookieContainer); Assert.Equal(expectedBool, actual.UseProxy); Assert.Equal(expectedBool, actual.UseTracing); } [Fact] public void Ctor_FileHttpHandlerOptions_MaxConnectionsPerServer() { // Arrange FileHttpHandlerOptions from = new() { MaxConnectionsPerServer = null, }; // Act, Assert HttpHandlerOptions actual = new(from); Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer); from.MaxConnectionsPerServer = 0; actual = new(from); Assert.Equal(int.MaxValue, actual.MaxConnectionsPerServer); from.MaxConnectionsPerServer = 111; actual = new(from); Assert.Equal(111, actual.MaxConnectionsPerServer); } [Theory] [InlineData(false, true, false)] [InlineData(true, null, false)] [InlineData(true, false, false)] [InlineData(true, true, true)] public void Ctor_FileHttpHandlerOptions_bool(bool useTracing, bool? fromUseTracing, bool expected) { // Arrange FileHttpHandlerOptions from = new() { MaxConnectionsPerServer = 333, UseTracing = fromUseTracing, }; // Act, Assert HttpHandlerOptions actual = new(from, useTracing); Assert.Equal(333, actual.MaxConnectionsPerServer); Assert.Equal(expected, actual.UseTracing); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/HttpVersionPolicyCreatorTests.cs ================================================ using Ocelot.Configuration.Creator; namespace Ocelot.UnitTests.Configuration; [Trait("Feat", "1672")] public sealed class HttpVersionPolicyCreatorTests : UnitTest { private readonly HttpVersionPolicyCreator _creator = new(); public HttpVersionPolicyCreatorTests() { } [Theory] [InlineData(VersionPolicies.RequestVersionOrLower, HttpVersionPolicy.RequestVersionOrLower)] [InlineData(VersionPolicies.RequestVersionExact, HttpVersionPolicy.RequestVersionExact)] [InlineData(VersionPolicies.RequestVersionOrHigher, HttpVersionPolicy.RequestVersionOrHigher)] public void Should_create_version_policy_based_on_input(string versionPolicy, HttpVersionPolicy expected) { // Arrange, Act var actual = _creator.Create(versionPolicy); // Assert Assert.Equal(expected, actual); } [Theory] [InlineData("")] [InlineData(null)] [InlineData("invalid version")] public void Should_default_to_request_version_or_lower(string versionPolicy) { // Arrange, Act var actual = _creator.Create(versionPolicy); // Assert Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); } [Fact] public void Should_default_to_request_version_or_lower_when_setting_gibberish() { // Arrange, Act var actual = _creator.Create("string is gibberish"); // Assert Assert.Equal(HttpVersionPolicy.RequestVersionOrLower, actual); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/MetadataOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; using System.Globalization; namespace Ocelot.UnitTests.Configuration; public class MetadataOptionsTests { [Fact] public void Ctor_Parameterless() { // Arrange, Act MetadataOptions actual = new(); // Assert Assert.Equal(CultureInfo.CurrentCulture, actual.CurrentCulture); Assert.Equal(NumberStyles.Any, actual.NumberStyle); Assert.Contains(",", actual.Separators); Assert.Equal(StringSplitOptions.None, actual.StringSplitOption); Assert.Contains(' ', actual.TrimChars); Assert.NotNull(actual.Metadata); } [Fact] [Trait("PR", "2324")] public void Ctor_CopyingFrom_MetadataOptions() { // Arrange MetadataOptions from = new( separators: ["x"], trimChars: ['y'], stringSplitOption: StringSplitOptions.TrimEntries, numberStyle: NumberStyles.Number, currentCulture: CultureInfo.GetCultureInfo("uk"), metadata: new Dictionary() { { "key", "value" }, }); // Act MetadataOptions actual = new(from); // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); } [Fact] [Trait("PR", "2324")] public void Ctor_CopyingFrom_FileMetadataOptions() { // Arrange FileMetadataOptions from = new() { CurrentCulture = "uk", NumberStyle = nameof(NumberStyles.Number), Separators = ["x"], StringSplitOption = nameof(StringSplitOptions.RemoveEmptyEntries), TrimChars = [';'], }; // Act MetadataOptions actual = new(from); // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equal(CultureInfo.GetCultureInfo("uk"), actual.CurrentCulture); Assert.Equal(NumberStyles.Number, actual.NumberStyle); Assert.Contains("x", actual.Separators); Assert.Equal(StringSplitOptions.RemoveEmptyEntries, actual.StringSplitOption); Assert.Contains(';', actual.TrimChars); Assert.NotNull(actual.Metadata); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Parser/ParsingConfigurationHeaderErrorTests.cs ================================================ using Ocelot.Configuration.Parser; using Ocelot.Errors; namespace Ocelot.UnitTests.Configuration.Parser; public class ParsingConfigurationHeaderErrorTests : UnitTest { [Fact] public void Ctor() { Exception exception = new(TestID); ParsingConfigurationHeaderError error = new(exception); Assert.Equal($"Parsing configuration exception is {TestID}", error.Message); Assert.Equal(OcelotErrorCode.ParsingConfigurationHeaderError, error.Code); Assert.Equal(404, error.HttpStatusCode); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Repository/ConsulFileConfigurationPollerOptionTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Provider.Consul; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration.Repository; public class ConsulFileConfigurationPollerOptionTests { private readonly Mock _mockInternalConfigRepo = new(); private readonly Mock _mockFileConfigurationRepository = new(); private readonly ConsulFileConfigurationPollerOption _sut; // System Under Test public ConsulFileConfigurationPollerOptionTests() { _sut = new( _mockInternalConfigRepo.Object, _mockFileConfigurationRepository.Object); } [Fact] public void Constructor_ShouldSetDependencies() { // Arrange & Act var result = _sut; // Assert Assert.NotNull(result); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenFileConfigurationIsNull() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnFileConfigPollingInterval_WhenFileConfigHasValidPollingInterval() { // Arrange const int expectedDelay = 5000; var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = expectedDelay } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(expectedDelay, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenFileConfigPollingIntervalIsZero() { // Arrange var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = 0 } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenFileConfigIsError() { // Arrange var err = new UnableToSetConfigInConsulError("Error message"); var fileConfigResponse = new ErrorResponse(err); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenFileConfigServiceDiscoveryProviderIsNull() { // Arrange var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = null } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnInternalConfigPollingInterval_WhenFileConfigFailsButInternalConfigIsValid() { // Arrange const int expectedDelay = 3000; var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfiguration = new InternalConfiguration { ServiceProviderConfiguration = new() { PollingInterval = expectedDelay, } }; var internalConfigResponse = new OkResponse(internalConfiguration); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(expectedDelay, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenInternalConfigPollingIntervalIsZero() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfiguration = new InternalConfiguration { ServiceProviderConfiguration = new() { PollingInterval = 0, } }; var internalConfigResponse = new OkResponse(internalConfiguration); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenInternalConfigIsError() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var err = new UnableToSetConfigInConsulError("Error message"); var internalConfigResponse = new ErrorResponse(err); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldReturnDefaultValue_WhenInternalConfigServiceProviderConfigurationIsNull() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfiguration = new InternalConfiguration { ServiceProviderConfiguration = null }; var internalConfigResponse = new OkResponse(internalConfiguration); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldPreferFileConfigOverInternalConfig_WhenBothHaveValidPollingIntervals() { // Arrange const int fileConfigDelay = 5000; const int internalConfigDelay = 3000; var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = fileConfigDelay, } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfiguration = new InternalConfiguration { ServiceProviderConfiguration = new ServiceProviderConfiguration { PollingInterval = internalConfigDelay } }; var internalConfigResponse = new OkResponse(internalConfiguration); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(fileConfigDelay, delay); } [Fact] public void Delay_ShouldReturn1000_WhenPollingIntervalIsNegative() { // Arrange const int negativeDelay = -100; var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = negativeDelay, } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); // Act var delay = _sut.Delay; // Assert // Note: The current implementation allows negative values to pass through // This test documents current behavior; consider if validation is needed Assert.Equal(1000, delay); } [Fact] public void Delay_ShouldCallFileConfigurationRepositoryGet() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert _mockFileConfigurationRepository.Verify(x => x.Get(), Times.Once); } [Fact] public void Delay_ShouldCallInternalConfigRepositoryGet_WhenFileConfigDoesNotHaveValidPollingInterval() { // Arrange var fileConfigResponse = new OkResponse(null); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); var internalConfigResponse = new OkResponse(null); _mockInternalConfigRepo .Setup(x => x.Get()) .Returns(internalConfigResponse); // Act var delay = _sut.Delay; // Assert _mockInternalConfigRepo.Verify(x => x.Get(), Times.Once); } [Fact] public void Delay_ShouldNotCallInternalConfigRepositoryGet_WhenFileConfigHasValidPollingInterval() { // Arrange var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = 5000, } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); // Act var delay = _sut.Delay; // Assert _mockInternalConfigRepo.Verify(x => x.Get(), Times.Never); } [Theory] [InlineData(100)] [InlineData(1000)] [InlineData(5000)] [InlineData(10000)] public void Delay_ShouldReturnValidPollingInterval_WithVariousValues(int pollingInterval) { // Arrange var fileConfiguration = new FileConfiguration { GlobalConfiguration = new() { ServiceDiscoveryProvider = new() { PollingInterval = pollingInterval } } }; var fileConfigResponse = new OkResponse(fileConfiguration); _mockFileConfigurationRepository .Setup(x => x.Get()) .ReturnsAsync(fileConfigResponse); // Act var delay = _sut.Delay; // Assert Assert.Equal(pollingInterval, delay); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Repository/DiskFileConfigurationRepositoryTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Newtonsoft.Json; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.DependencyInjection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Configuration.Repository; public sealed class DiskFileConfigurationRepositoryTests : FileUnitTest { private readonly Mock _hostingEnvironment; private readonly Mock _changeTokenSource; private DiskFileConfigurationRepository _repo; private FileConfiguration _result; public DiskFileConfigurationRepositoryTests() { _hostingEnvironment = new Mock(); _changeTokenSource = new Mock(MockBehavior.Strict); _changeTokenSource.Setup(m => m.Activate()); } private void Arrange([CallerMemberName] string testName = null) { _hostingEnvironment.Setup(he => he.EnvironmentName).Returns(testName); _repo = new DiskFileConfigurationRepository(_hostingEnvironment.Object, _changeTokenSource.Object, TestID); } [Fact] public async Task Should_return_file_configuration() { Arrange(); var config = FakeFileConfigurationForGet(); GivenTheConfigurationIs(config); // Act _result = (await _repo.Get()).Data; // Assert ThenTheFollowingIsReturned(config); } [Fact] public async Task Should_return_file_configuration_if_environment_name_is_unavailable() { Arrange(); var config = FakeFileConfigurationForGet(); GivenTheEnvironmentNameIsUnavailable(); GivenTheConfigurationIs(config); // Act _result = (await _repo.Get()).Data; // Assert ThenTheFollowingIsReturned(config); } [Fact] public async Task Should_set_file_configuration() { Arrange(); var config = FakeFileConfigurationForSet(); // Act await WhenISetTheConfiguration(config); // Assert ThenTheConfigurationIsStoredAs(config); ThenTheConfigurationJsonIsIndented(config); _changeTokenSource.Verify(m => m.Activate(), Times.Once); // and the change token is activated } [Fact] public async Task Should_set_file_configuration_if_environment_name_is_unavailable() { Arrange(); var config = FakeFileConfigurationForSet(); GivenTheEnvironmentNameIsUnavailable(); // Act await WhenISetTheConfiguration(config); // Assert ThenTheConfigurationIsStoredAs(config); ThenTheConfigurationJsonIsIndented(config); } [Fact] public async Task Should_set_environment_file_configuration_and_ocelot_file_configuration() { Arrange(); var config = FakeFileConfigurationForSet(); GivenTheConfigurationIs(config); var ocelotJson = GivenTheUserAddedOcelotJson(); // Act await WhenISetTheConfiguration(config); // Assert ThenTheConfigurationIsStoredAs(config); ThenTheConfigurationJsonIsIndented(config); ThenTheOcelotJsonIsStoredAs(ocelotJson, config); } private FileInfo GivenTheUserAddedOcelotJson() { var primaryFile = Path.Combine(TestID, ConfigurationBuilderExtensions.PrimaryConfigFile); var ocelotJson = new FileInfo(primaryFile); if (ocelotJson.Exists) { ocelotJson.Delete(); } File.WriteAllText(ocelotJson.FullName, "Doesnt matter"); files.Add(ocelotJson.FullName); return ocelotJson; } private void GivenTheEnvironmentNameIsUnavailable() { _hostingEnvironment.Setup(he => he.EnvironmentName).Returns((string)null); } private async Task WhenISetTheConfiguration(FileConfiguration fileConfiguration) { await _repo.Set(fileConfiguration); var response = await _repo.Get(); _result = response.Data; } private void ThenTheConfigurationIsStoredAs(FileConfiguration expecteds) { _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); for (var i = 0; i < _result.Routes.Count; i++) { for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++) { var result = _result.Routes[i].DownstreamHostAndPorts[j]; var expected = expecteds.Routes[i].DownstreamHostAndPorts[j]; result.Host.ShouldBe(expected.Host); result.Port.ShouldBe(expected.Port); } _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate); _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme); } } private static void ThenTheOcelotJsonIsStoredAs(FileInfo ocelotJson, FileConfiguration expecteds) { var actual = File.ReadAllText(ocelotJson.FullName); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); actual.ShouldBe(expectedText); } private void GivenTheConfigurationIs(FileConfiguration fileConfiguration, [CallerMemberName] string environmentName = null) { var environmentSpecificPath = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); var environmentSpecific = new FileInfo(environmentSpecificPath); if (environmentSpecific.Exists) { environmentSpecific.Delete(); } File.WriteAllText(environmentSpecific.FullName, jsonConfiguration); files.Add(environmentSpecific.FullName); } private void ThenTheConfigurationJsonIsIndented(FileConfiguration expecteds, [CallerMemberName] string environmentName = null) { var environmentSpecific = Path.Combine(TestID, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, environmentName)); var actual = File.ReadAllText(environmentSpecific); var expectedText = JsonConvert.SerializeObject(expecteds, Formatting.Indented); actual.ShouldBe(expectedText); files.Add(environmentSpecific); } private void ThenTheFollowingIsReturned(FileConfiguration expecteds) { _result.GlobalConfiguration.RequestIdKey.ShouldBe(expecteds.GlobalConfiguration.RequestIdKey); _result.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); _result.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Host); _result.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(expecteds.GlobalConfiguration.ServiceDiscoveryProvider.Port); for (var i = 0; i < _result.Routes.Count; i++) { for (var j = 0; j < _result.Routes[i].DownstreamHostAndPorts.Count; j++) { var result = _result.Routes[i].DownstreamHostAndPorts[j]; var expected = expecteds.Routes[i].DownstreamHostAndPorts[j]; result.Host.ShouldBe(expected.Host); result.Port.ShouldBe(expected.Port); } _result.Routes[i].DownstreamPathTemplate.ShouldBe(expecteds.Routes[i].DownstreamPathTemplate); _result.Routes[i].DownstreamScheme.ShouldBe(expecteds.Routes[i].DownstreamScheme); } } private static FileConfiguration FakeFileConfigurationForSet() { var route = GivenRoute("123.12.12.12", "/asdfs/test/{test}"); return GivenConfiguration(route); } private static FileConfiguration FakeFileConfigurationForGet() { var route = GivenRoute("localhost", "/test/test/{test}"); return GivenConfiguration(route); } private static FileRoute GivenRoute(string host, string downstream) => new() { DownstreamHostAndPorts = new() { new(host, 80) }, DownstreamScheme = Uri.UriSchemeHttps, DownstreamPathTemplate = downstream, }; private static FileConfiguration GivenConfiguration(params FileRoute[] routes) { var config = new FileConfiguration(); config.Routes.AddRange(routes); config.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = "https", Port = 198, Host = "blah", }; return config; } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Repository/FileConfigurationPollerTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Logging; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Configuration.Repository; public sealed class FileConfigurationPollerTests : UnitTest, IDisposable { private const int PollingDelayInMs = 100; private const int LongRunningPollDelayInMs = PollingDelayInMs + 50; private readonly FileConfigurationPoller _poller; private readonly Mock _factory; private readonly Mock _repo; private readonly FileConfiguration _initialFileConfig; private readonly Mock _config; private readonly Mock _internalConfigRepo; private readonly Mock _internalConfigCreator; private readonly Mock _internalConfig; public FileConfigurationPollerTests() { var logger = new Mock(); _factory = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(logger.Object); _repo = new Mock(); _initialFileConfig = new FileConfiguration(); _config = new Mock(); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(_initialFileConfig)); _config.Setup(x => x.Delay).Returns(PollingDelayInMs); _internalConfig = new Mock(); _internalConfigRepo = new Mock(); _internalConfigCreator = new Mock(); _internalConfigCreator.Setup(x => x.Create(It.IsAny())).ReturnsAsync(new OkResponse(_internalConfig.Object)); _poller = new FileConfigurationPoller(_factory.Object, _repo.Object, _config.Object, _internalConfigRepo.Object, _internalConfigCreator.Object); } [Fact] public void Should_start_and_poll_initial_configuration() { // Arrange, Act _poller.StartAsync(CancellationToken.None); // Assert ThenTheSetterIsCalled(_initialFileConfig, 1); } [Fact] public async Task Should_not_replace_timer_when_start_called_twice() { // Arrange await _poller.StartAsync(TestContext.Current.CancellationToken); var timerAfterFirstStart = CurrentTimer(); // Act await _poller.StartAsync(TestContext.Current.CancellationToken); var timerAfterSecondStart = CurrentTimer(); // Assert timerAfterFirstStart.ShouldNotBeNull(); timerAfterSecondStart.ShouldBeSameAs(timerAfterFirstStart); } [Fact] public async Task Should_do_nothing_when_stop_called_before_start() { // Arrange, Act await _poller.StopAsync(TestContext.Current.CancellationToken); await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken); // Assert NumberOfGetInvocations().ShouldBe(0); } [Fact] public void Should_call_setter_when_gets_new_config() { // Arrange var newConfig = GivenConfiguration(); // Act _poller.StartAsync(CancellationToken.None); // Assert WhenTheConfigIsChanged(newConfig, 0); ThenTheSetterIsCalled(newConfig, 1); } [Fact] public void Should_call_setter_only_once_when_configuration_does_not_change_across_multiple_poll_cycles() { // Arrange, Act _poller.StartAsync(CancellationToken.None); // Assert ThenTheSetterIsCalled(_initialFileConfig, 1); ThenTheConfigIsNotAddedMoreThan(1); } [Fact] public void Should_not_poll_if_already_polling() { // Arrange var newConfig = GivenConfiguration(); // Act _poller.StartAsync(CancellationToken.None); // Assert WhenTheConfigIsChanged(newConfig, LongRunningPollDelayInMs); ThenTheSetterIsCalled(newConfig, 1); } [Fact] public async Task Should_return_early_on_timer_tick_when_polling_is_already_in_progress() { // Arrange var getTaskSource = new TaskCompletionSource>(TaskCreationOptions.RunContinuationsAsynchronously); var getCallCount = 0; _repo.Setup(x => x.Get()).Returns(() => { Interlocked.Increment(ref getCallCount); return getTaskSource.Task; }); // Act await _poller.StartAsync(TestContext.Current.CancellationToken); await Task.Delay(PollingDelayInMs * 3, TestContext.Current.CancellationToken); // Assert getCallCount.ShouldBe(1); // Cleanup getTaskSource.SetResult(new OkResponse(_initialFileConfig)); await _poller.StopAsync(TestContext.Current.CancellationToken); } [Fact] public void Should_do_nothing_if_call_to_provider_fails() { // Arrange, Act WhenProviderErrors(); _poller.StartAsync(CancellationToken.None); // Assert ThenTheProviderIsPolled(); ThenTheSetterIsNotCalled(); } [Fact] public void Should_not_add_to_internal_repo_if_internal_configuration_creation_fails() { // Arrange var newConfig = GivenConfiguration(); _internalConfigCreator .Setup(x => x.Create(It.IsAny())) .ReturnsAsync(new ErrorResponse(new AnyError())); _repo.Setup(x => x.Get()).ReturnsAsync(new OkResponse(newConfig)); // Act _poller.StartAsync(CancellationToken.None); // Assert ThenTheCreatorIsCalled(newConfig, 1); ThenTheConfigIsNotAdded(); } [Fact] public async Task Should_stop_polling_when_stopped() { // Arrange, Act await _poller.StartAsync(TestContext.Current.CancellationToken); await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken); await _poller.StopAsync(TestContext.Current.CancellationToken); await Task.Delay(PollingDelayInMs, TestContext.Current.CancellationToken); var afterStopSettled = NumberOfGetInvocations(); await Task.Delay(PollingDelayInMs * 2, TestContext.Current.CancellationToken); // Assert ThenTheSetterIsCalled(_initialFileConfig, 1); NumberOfGetInvocations().ShouldBe(afterStopSettled); } [Fact] public void Should_dispose_cleanly_without_starting() { // Arrange, Act, Assert _poller.Dispose(); // when poller is disposed } private static FileConfiguration GivenConfiguration() => new() { Routes = new() { new() { DownstreamHostAndPorts = [ new("test", 80) ], }, }, }; private void WhenProviderErrors() { _repo .Setup(x => x.Get()) .ReturnsAsync(new ErrorResponse(new AnyError())); } private void WhenTheConfigIsChanged(FileConfiguration newConfig, int delay) { _repo .Setup(x => x.Get()) .Callback(() => Thread.Sleep(delay)) .ReturnsAsync(new OkResponse(newConfig)); } private bool AssertWhile(Action assertion, int milliSeconds = 4_000) { bool TryAssert() { try { assertion.Invoke(); return true; } catch (Exception) { return false; } } return Wait.For(milliSeconds).Until(TryAssert); } private void ThenTheSetterIsCalled(FileConfiguration fileConfig, int times) { var result = AssertWhile(() => { _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.Exactly(times)); _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times)); }); Assert.True(result); } private void ThenTheSetterIsNotCalled() { _internalConfigRepo.Verify(x => x.AddOrReplace(It.IsAny()), Times.Never); _internalConfigCreator.Verify(x => x.Create(It.IsAny()), Times.Never); } private void ThenTheCreatorIsCalled(FileConfiguration fileConfig, int times) { var result = AssertWhile(() => { _internalConfigCreator.Verify(x => x.Create(fileConfig), Times.Exactly(times)); }); Assert.True(result); } private void ThenTheCreatorIsCalled(int times) { var result = AssertWhile(() => { _internalConfigCreator.Verify(x => x.Create(It.IsAny()), Times.Exactly(times)); }); Assert.True(result); } private void ThenTheConfigIsNotAdded() { _internalConfigRepo.Verify(x => x.AddOrReplace(It.IsAny()), Times.Never); } private void ThenTheConfigIsNotAddedMoreThan(int times) { var result = AssertWhile(() => { _internalConfigRepo.Verify(x => x.AddOrReplace(_internalConfig.Object), Times.Exactly(times)); }); Assert.True(result); } private int NumberOfGetInvocations() { return _repo.Invocations.Count(x => x.Method.Name == nameof(IFileConfigurationRepository.Get)); } private Timer CurrentTimer() { var timerField = typeof(FileConfigurationPoller) .GetField("_timer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); timerField.ShouldNotBeNull(); return timerField.GetValue(_poller) as Timer; } private void ThenTheProviderIsPolled() { var result = AssertWhile(() => { _repo.Verify(x => x.Get(), Times.AtLeastOnce()); }); Assert.True(result); } public void Dispose() { _poller.Dispose(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Repository/InMemoryConfigurationRepositoryTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.ChangeTracking; using Ocelot.Configuration.Repository; using Ocelot.Responses; namespace Ocelot.UnitTests.Configuration.Repository; public class InMemoryConfigurationRepositoryTests : UnitTest { private readonly InMemoryInternalConfigurationRepository _repo; private IInternalConfiguration _config; private Response _result; private Response _getResult; private readonly Mock _changeTokenSource; public InMemoryConfigurationRepositoryTests() { _changeTokenSource = new Mock(MockBehavior.Strict); _changeTokenSource.Setup(m => m.Activate()); _repo = new InMemoryInternalConfigurationRepository(_changeTokenSource.Object); } [Fact] public void Can_add_config() { // Arrange _config = new FakeConfig("initial", "adminath"); // Act _result = _repo.AddOrReplace(_config); // Assert _result.IsError.ShouldBeFalse(); _changeTokenSource.Verify(m => m.Activate(), Times.Once); } [Fact] public void Can_get_config() { // Arrange _config = new FakeConfig("initial", "adminath"); _result = _repo.AddOrReplace(_config); // Act _getResult = _repo.Get(); // Assert _getResult.Data.Routes[0].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("initial"); } private class FakeConfig : IInternalConfiguration { private readonly string _downstreamTemplatePath; public FakeConfig(string downstreamTemplatePath, string administrationPath) { _downstreamTemplatePath = downstreamTemplatePath; AdministrationPath = administrationPath; } public Route[] Routes { get { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate(_downstreamTemplatePath) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); return new Route[] { new(downstreamRoute, HttpMethod.Get), }; } } public string AdministrationPath { get; } public AuthenticationOptions AuthenticationOptions { get; } public ServiceProviderConfiguration ServiceProviderConfiguration => throw new NotImplementedException(); public string RequestId { get; } public LoadBalancerOptions LoadBalancerOptions { get; } public string DownstreamScheme { get; } public QoSOptions QoSOptions { get; } public HttpHandlerOptions HttpHandlerOptions { get; } public Version DownstreamHttpVersion { get; } public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public MetadataOptions MetadataOptions => throw new NotImplementedException(); public RateLimitOptions RateLimitOptions => throw new NotImplementedException(); public int? Timeout => throw new NotImplementedException(); public CacheOptions CacheOptions => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Repository/InMemoryFileConfigurationPollerOptionsTests.cs ================================================ using Ocelot.Configuration.Repository; namespace Ocelot.UnitTests.Configuration.Repository; public class InMemoryFileConfigurationPollerOptionsTests { [Fact] public void Delay() { InMemoryFileConfigurationPollerOptions sut = new(); Assert.Equal(1000, sut.Delay); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/RequestIdKeyCreatorTests.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class RequestIdKeyCreatorTests : UnitTest { private readonly RequestIdKeyCreator _creator = new(); [Fact] public void Should_use_global_configuration() { // Arrange var route = new FileRoute(); var globalConfig = new FileGlobalConfiguration { RequestIdKey = "cheese", }; // Act var result = _creator.Create(route, globalConfig); // Assert result.ShouldBe("cheese"); } [Fact] public void Should_use_re_route_specific() { // Arrange var route = new FileRoute { RequestIdKey = "cheese", }; var globalConfig = new FileGlobalConfiguration(); // Act var result = _creator.Create(route, globalConfig); // Assert result.ShouldBe("cheese"); } [Fact] public void Should_use_re_route_over_global_specific() { // Arrange var route = new FileRoute { RequestIdKey = "cheese", }; var globalConfig = new FileGlobalConfiguration { RequestIdKey = "test", }; // Act var result = _creator.Create(route, globalConfig); // Assert result.ShouldBe("cheese"); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/RouteKeyCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.LoadBalancer.Balancers; namespace Ocelot.UnitTests.Configuration; public class RouteKeyCreatorTests : UnitTest { private readonly RouteKeyCreator _creator = new(); [Fact] public void Should_return_sticky_session_key() { // Arrange FileRoute route = new(); LoadBalancerOptions options = new(nameof(CookieStickySessions), "testy", null); // Act var result = _creator.Create(route, options); // Assert result.ShouldBe("CookieStickySessions:testy"); } [Fact] public void Should_return_route_key() { // Arrange var route = new FileRoute { UpstreamPathTemplate = "/api/product", UpstreamHttpMethod = ["GET", "POST", "PUT"], DownstreamHostAndPorts = new() { new("localhost", 8080), new("localhost", 4430), }, }; LoadBalancerOptions options = new(); // Act var result = _creator.Create(route, options); // Assert result.ShouldBe("GET,POST,PUT|/api/product|no-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key"); } [Fact] public void Should_return_route_key_with_upstream_host() { // Arrange var route = new FileRoute { UpstreamHost = "my-host", UpstreamPathTemplate = "/api/product", UpstreamHttpMethod = ["GET", "POST", "PUT"], DownstreamHostAndPorts = new() { new("localhost", 8080), new("localhost", 4430), }, }; LoadBalancerOptions options = new(); // Act var result = _creator.Create(route, options); // Assert result.ShouldBe("GET,POST,PUT|/api/product|my-host|localhost:8080,localhost:4430|no-svc-ns|no-svc-name|NoLoadBalancer|no-lb-key"); } [Fact] public void Should_return_route_key_with_svc_name() { // Arrange var route = new FileRoute { UpstreamPathTemplate = "/api/product", UpstreamHttpMethod = ["GET", "POST", "PUT"], ServiceName = "products-service", }; LoadBalancerOptions options = new(); // Act var result = _creator.Create(route, options); // Assert result.ShouldBe("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|NoLoadBalancer|no-lb-key"); } [Fact] public void Should_return_route_key_with_load_balancer_options() { // Arrange var route = new FileRoute { UpstreamPathTemplate = "/api/product", UpstreamHttpMethod = ["GET", "POST", "PUT"], ServiceName = "products-service", LoadBalancerOptions = new FileLoadBalancerOptions { Type = nameof(LeastConnection), Key = "testy", }, }; LoadBalancerOptions options = new(route.LoadBalancerOptions); // Act var result = _creator.Create(route, options); // Assert result.ShouldBe("GET,POST,PUT|/api/product|no-host|no-host-and-port|no-svc-ns|products-service|LeastConnection|testy"); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_FileDynamicRoute_TryStickySession() { // Arrange FileDynamicRoute route = new(); LoadBalancerOptions options = new(nameof(CookieStickySessions), "TestKey", null); // Act var actual = _creator.Create(route, options); // Assert Assert.Equal("CookieStickySessions:TestKey", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_FileDynamicRoute_HasLoadBalancingKey() { // Arrange FileDynamicRoute route = new(); LoadBalancerOptions options = new(nameof(RoundRobin), "LBKey", null); // Act var actual = _creator.Create(route, options); // Assert Assert.Equal("LBKey", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_FileDynamicRoute_NoLBKey() { // Arrange FileDynamicRoute route = new() { ServiceName = "test", ServiceNamespace = "namespace", }; LoadBalancerOptions options = new(nameof(RoundRobin), null, null); // Act var actual = _creator.Create(route, options); // Assert Assert.Equal("namespace.test", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_String_String_TryStickySession() { // Arrange LoadBalancerOptions options = new(nameof(CookieStickySessions), "TestKey", null); // Act var actual = _creator.Create("namespace", "service", options); // Assert Assert.Equal("CookieStickySessions:TestKey", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_String_String_HasLoadBalancingKey() { // Arrange LoadBalancerOptions options = new(nameof(RoundRobin), "LBKey", null); // Act var actual = _creator.Create("namespace", "service", options); // Assert Assert.Equal("LBKey", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void Create_String_String_NoLBKey() { // Arrange LoadBalancerOptions options = new(nameof(RoundRobin), null, null); // Act var actual = _creator.Create("namespace", "service", options); // Assert Assert.Equal("namespace.service", actual); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] public void AsString() { // Arrange, Act, Assert FileHostAndPort host = null; var actual = RouteKeyCreator.AsString(host); Assert.Null(actual); // Arrange, Act, Assert host = new("test.host", 123); actual = RouteKeyCreator.AsString(host); Assert.Equal("test.host:123", actual); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/RouteTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; namespace Ocelot.UnitTests.Configuration; public class RouteTests { [Fact] public void Ctor() { // Arrange, Act Route r = new(); // Assert Assert.NotNull(r.DownstreamRoute); Assert.Empty(r.DownstreamRoute); Assert.False(r.IsDynamic); Assert.Null(r.Aggregator); Assert.NotNull(r.DownstreamRoute); Assert.Null(r.DownstreamRouteConfig); Assert.Null(r.UpstreamHeaderTemplates); Assert.Null(r.UpstreamHost); Assert.Null(r.UpstreamHttpMethod); Assert.Null(r.UpstreamTemplatePattern); } [Fact] public void Ctor_Boolean() { // Arrange, Act Route r1 = new(true), r2 = new(false); Assert.True(r1.IsDynamic); Assert.False(r2.IsDynamic); Assert.Empty(r1.DownstreamRoute); Assert.Empty(r2.DownstreamRoute); } [Fact] public void Ctor_Boolean_DownstreamRoute() { // Arrange DownstreamRoute route = new DownstreamRouteBuilder().Build(); // Act Route r = new(true, route); Assert.True(r.IsDynamic); Assert.NotEmpty(r.DownstreamRoute); Assert.Equal(route, r.DownstreamRoute[0]); } [Fact] public void Ctor_DownstreamRoute() { // Arrange DownstreamRoute route = new DownstreamRouteBuilder().Build(); // Act Route r = new(route); Assert.False(r.IsDynamic); Assert.NotEmpty(r.DownstreamRoute); Assert.Equal(route, r.DownstreamRoute[0]); } [Fact] public void Ctor_DownstreamRoute_HttpMethod() { // Arrange DownstreamRoute route = new DownstreamRouteBuilder().Build(); HttpMethod method = HttpMethod.Connect; // Act Route r = new(route, method); Assert.NotEmpty(r.DownstreamRoute); Assert.Equal(route, r.DownstreamRoute[0]); Assert.NotEmpty(r.UpstreamHttpMethod); Assert.Equal(method, r.UpstreamHttpMethod.First()); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/SecurityOptionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public sealed class SecurityOptionsCreatorTests : UnitTest { private readonly SecurityOptionsCreator _creator = new(); [Fact] public void Should_create_route_security_config() { // Arrange var ipAllowedList = new List { "127.0.0.1", "192.168.1.1" }; var ipBlockedList = new List { "127.0.0.1", "192.168.1.1" }; var securityOptions = new FileSecurityOptions { IPAllowedList = ipAllowedList, IPBlockedList = ipBlockedList, }; var expected = new SecurityOptions(ipAllowedList, ipBlockedList); var globalConfig = new FileGlobalConfiguration(); // Act var actual = _creator.Create(securityOptions, globalConfig); // Assert actual.ThenTheResultIs(expected); } [Fact] [Trait("Feat", "2170")] public void Should_create_global_security_config() { // Arrange var ipAllowedList = new List { "127.0.0.1", "192.168.1.1" }; var ipBlockedList = new List { "127.0.0.1", "192.168.1.1" }; var globalConfig = new FileGlobalConfiguration { SecurityOptions = new() { IPAllowedList = ipAllowedList, IPBlockedList = ipBlockedList, }, }; var expected = new SecurityOptions(ipAllowedList, ipBlockedList); // Act var actual = _creator.Create(new(), globalConfig); // Assert actual.ThenTheResultIs(expected); } [Fact] [Trait("Feat", "2170")] public void Should_create_global_route_security_config() { // Arrange var routeIpAllowedList = new List { "127.0.0.1", "192.168.1.1" }; var routeIpBlockedList = new List { "127.0.0.1", "192.168.1.1" }; var securityOptions = new FileSecurityOptions { IPAllowedList = routeIpAllowedList, IPBlockedList = routeIpBlockedList, }; var globalIpAllowedList = new List { "127.0.0.2", "192.168.1.2" }; var globalIpBlockedList = new List { "127.0.0.2", "192.168.1.2" }; var globalConfig = new FileGlobalConfiguration { SecurityOptions = new FileSecurityOptions { IPAllowedList = globalIpAllowedList, IPBlockedList = globalIpBlockedList, }, }; var expected = new SecurityOptions(routeIpAllowedList, routeIpBlockedList); // Act var actual = _creator.Create(securityOptions, globalConfig); // Assert actual.ThenTheResultIs(expected); } } internal static class SecurityOptionsExtensions { public static void ThenTheResultIs(this SecurityOptions actual, SecurityOptions expected) { for (var i = 0; i < expected.IPAllowedList.Count; i++) { actual.IPAllowedList[i].ShouldBe(expected.IPAllowedList[i]); } for (var i = 0; i < expected.IPBlockedList.Count; i++) { actual.IPBlockedList[i].ShouldBe(expected.IPBlockedList[i]); } } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/ServiceProviderConfigurationCreatorTests.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class ServiceProviderConfigurationCreatorTests : UnitTest { private readonly ServiceProviderConfigurationCreator _creator = new(); [Fact] public void Should_create_service_provider_config() { // Arrange var globalConfig = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = "https", Host = "127.0.0.1", Port = 1234, Type = "ServiceFabric", Token = "testtoken", ConfigurationKey = "woo", Namespace = "default", }, }; var expected = new ServiceProviderConfigurationBuilder() .WithScheme("https") .WithHost("127.0.0.1") .WithPort(1234) .WithType("ServiceFabric") .WithToken("testtoken") .WithConfigurationKey("woo") .WithNamespace("default") .Build(); // Act var result = _creator.Create(globalConfig); // Assert result.Scheme.ShouldBe(expected.Scheme); result.Host.ShouldBe(expected.Host); result.Port.ShouldBe(expected.Port); result.Token.ShouldBe(expected.Token); result.Type.ShouldBe(expected.Type); result.Namespace.ShouldBe(expected.Namespace); result.ConfigurationKey.ShouldBe(expected.ConfigurationKey); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/StaticRoutesCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Values; namespace Ocelot.UnitTests.Configuration; public class StaticRoutesCreatorTests : UnitTest { private readonly StaticRoutesCreator _creator; private readonly Mock _cthCreator; private readonly Mock _aoCreator; private readonly Mock _utpCreator; private readonly Mock _uhtpCreator; private readonly Mock _ridkCreator; private readonly Mock _qosoCreator; private readonly Mock _rloCreator; private readonly Mock _coCreator; private readonly Mock _hhoCreator; private readonly Mock _hfarCreator; private readonly Mock _daCreator; private readonly Mock _lboCreator; private readonly Mock _rrkCreator; private readonly Mock _soCreator; private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; private readonly Mock _metadataCreator; private FileConfiguration _fileConfig; private string _requestId; private string _rrk; private UpstreamPathTemplate _upt; private AuthenticationOptions _ao; private List _ctt; private QoSOptions _qoso; private RateLimitOptions _rlo; private CacheOptions _cacheOptions; private HttpHandlerOptions _hho; private HeaderTransformations _ht; private List _dhp; private LoadBalancerOptions _lbo; private IReadOnlyList _result; private Version _expectedVersion; private HttpVersionPolicy _expectedVersionPolicy; private Dictionary _uht; private Dictionary _expectedMetadata; public StaticRoutesCreatorTests() { _cthCreator = new Mock(); _aoCreator = new Mock(); _utpCreator = new Mock(); _ridkCreator = new Mock(); _qosoCreator = new Mock(); _rloCreator = new Mock(); _coCreator = new Mock(); _hhoCreator = new Mock(); _hfarCreator = new Mock(); _daCreator = new Mock(); _lboCreator = new Mock(); _rrkCreator = new Mock(); _soCreator = new Mock(); _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); _uhtpCreator = new Mock(); _metadataCreator = new Mock(); _creator = new StaticRoutesCreator( _cthCreator.Object, _aoCreator.Object, _utpCreator.Object, _ridkCreator.Object, _qosoCreator.Object, _rloCreator.Object, _coCreator.Object, _hhoCreator.Object, _hfarCreator.Object, _daCreator.Object, _lboCreator.Object, _rrkCreator.Object, _soCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, _uhtpCreator.Object, _metadataCreator.Object); } [Fact] public void Should_return_nothing() { // Arrange _fileConfig = new FileConfiguration(); // Act _result = _creator.Create(_fileConfig); // Assert _result.ShouldBeEmpty(); } [Fact] public void Should_return_routes() { // Arrange _fileConfig = new FileConfiguration { Routes = new List { new() { ServiceName = "dave", DangerousAcceptAnyServerCertificateValidator = true, AddClaimsToRequest = new Dictionary { { "a","b" }, }, AddHeadersToRequest = new Dictionary { { "c","d" }, }, AddQueriesToRequest = new Dictionary { { "e","f" }, }, UpstreamHttpMethod = ["GET", "POST"], Metadata = new Dictionary { ["foo"] = "bar", }, LoadBalancerOptions = new("LB1"), CacheOptions = new() { Region = "dave" }, }, new() { ServiceName = "wave", DangerousAcceptAnyServerCertificateValidator = false, AddClaimsToRequest = new Dictionary { { "g","h" }, }, AddHeadersToRequest = new Dictionary { { "i","j" }, }, AddQueriesToRequest = new Dictionary { { "k","l" }, }, UpstreamHttpMethod = ["PUT", "DELETE"], Metadata = new Dictionary { ["foo"] = "baz", }, LoadBalancerOptions = new("LB2"), CacheOptions = new() { Region = "wave" }, }, }, }; GivenTheDependenciesAreSetUpCorrectly(); // Act _result = _creator.Create(_fileConfig); // Assert ThenTheDependenciesAreCalledCorrectly(); ThenTheRoutesAreCreated(); } #region PR 2073 [Fact] [Trait("PR", "2073")] // https://github.com/ThreeMammals/Ocelot/pull/2073 [Trait("Feat", "1314")] // https://github.com/ThreeMammals/Ocelot/issues/1314 [Trait("Feat", "1869")] // https://github.com/ThreeMammals/Ocelot/issues/1869 public void CreateTimeout_HasRouteTimeout_ShouldCreateFromRoute() { // Arrange var route = new FileRoute { Timeout = 11 }; var global = new FileGlobalConfiguration { Timeout = 22 }; // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Equal(route.Timeout, timeout); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] public void CreateTimeout_NoRouteTimeoutAndHasGlobalOne_ShouldCreateFromGlobalConfig() { // Arrange var route = new FileRoute(); var global = new FileGlobalConfiguration { Timeout = 22 }; // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Null(route.Timeout); Assert.Equal(global.Timeout, timeout); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] public void CreateTimeout_NoRouteTimeoutAndNoGlobalOne_ShouldCreateFromDownstreamRouteDefaults() { // Arrange var route = new FileRoute(); var global = new FileGlobalConfiguration(); // Act var timeout = _creator.CreateTimeout(route, global); // Assert Assert.Null(route.Timeout); Assert.Null(global.Timeout); Assert.Equal(DownstreamRoute.DefTimeout, timeout); } #endregion [Theory] [Trait("PR", "2294")] [InlineData("", 0)] [InlineData("A", 1)] [InlineData("A,B", 2)] [InlineData(" X ", 1)] public void SetUpRoute_FileRouteUpstreamHttpMethod(string methods, int count) { // Arrange _fileConfig = new FileConfiguration(); _fileConfig.Routes.Add(new() { UpstreamHttpMethod = methods.Split(',').Where(m => !string.IsNullOrEmpty(m)).ToHashSet() }); GivenTheDependenciesAreSetUpCorrectly(); // Act _result = _creator.Create(_fileConfig); // Assert Assert.Equal(count, _result[0].UpstreamHttpMethod.Count); } private void ThenTheDependenciesAreCalledCorrectly() { ThenTheDepsAreCalledFor(_fileConfig.Routes[0], _fileConfig.GlobalConfiguration); ThenTheDepsAreCalledFor(_fileConfig.Routes[1], _fileConfig.GlobalConfiguration); } private void GivenTheDependenciesAreSetUpCorrectly() { _expectedVersion = new Version("1.1"); _expectedVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; _requestId = "testy"; _rrk = "besty"; _upt = new UpstreamPathTemplateBuilder().Build(); _ao = new AuthenticationOptions(); _ctt = new List(); _qoso = new QoSOptions(); _rlo = new RateLimitOptions(); _cacheOptions = new CacheOptions(0, "vesty", null, false); _hho = new(); _ht = new HeaderTransformations(new List(), new List(), new List(), new List()); _dhp = new List(); _lbo = new(); _uht = new Dictionary(); _expectedMetadata = new Dictionary() { ["foo"] = "bar", }; _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); _rrkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rrk); _utpCreator.Setup(x => x.Create(It.IsAny())).Returns(_upt); _aoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ao); _cthCreator.Setup(x => x.Create(It.IsAny>())).Returns(_ctt); _qosoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_qoso); _rloCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_rlo); _coCreator.Setup(x => x.Create(It.IsAny(), It.IsAny(), It.IsAny())).Returns(_cacheOptions); _hhoCreator.Setup(x => x.Create(It.IsAny())).Returns(_hho); _hhoCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_hho); _hfarCreator.Setup(x => x.Create(It.IsAny())).Returns(_ht); _hfarCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_ht); _daCreator.Setup(x => x.Create(It.IsAny())).Returns(_dhp); _lboCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_lbo); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) .Returns(new MetadataOptions() { Metadata = _expectedMetadata }); } private void ThenTheRoutesAreCreated() { _result.Count.ShouldBe(2); ThenTheRouteIsSet(_fileConfig.Routes[0], 0); ThenTheRouteIsSet(_fileConfig.Routes[1], 1); } private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) { _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersion.ShouldBe(_expectedVersion); _result[routeIndex].DownstreamRoute[0].DownstreamHttpVersionPolicy.ShouldBe(_expectedVersionPolicy); _result[routeIndex].DownstreamRoute[0].IsAuthenticated.ShouldBeFalse(); _result[routeIndex].DownstreamRoute[0].IsAuthorized.ShouldBeFalse(); _result[routeIndex].DownstreamRoute[0].CacheOptions.UseCache.ShouldBeFalse(); _result[routeIndex].DownstreamRoute[0].RequestIdKey.ShouldBe(_requestId); _result[routeIndex].DownstreamRoute[0].LoadBalancerKey.ShouldBe(_rrk); _result[routeIndex].DownstreamRoute[0].UpstreamPathTemplate.ShouldBe(_upt); _result[routeIndex].DownstreamRoute[0].AuthenticationOptions.ShouldBe(_ao); _result[routeIndex].DownstreamRoute[0].ClaimsToHeaders.ShouldBe(_ctt); _result[routeIndex].DownstreamRoute[0].ClaimsToQueries.ShouldBe(_ctt); _result[routeIndex].DownstreamRoute[0].ClaimsToClaims.ShouldBe(_ctt); _result[routeIndex].DownstreamRoute[0].QosOptions.ShouldBe(_qoso); _result[routeIndex].DownstreamRoute[0].RateLimitOptions.ShouldBe(_rlo); _result[routeIndex].DownstreamRoute[0].CacheOptions.Region.ShouldBe(_cacheOptions.Region); _result[routeIndex].DownstreamRoute[0].CacheOptions.TtlSeconds.ShouldBe(0); _result[routeIndex].DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_hho); _result[routeIndex].DownstreamRoute[0].UpstreamHeadersFindAndReplace.ShouldBe(_ht.Upstream); _result[routeIndex].DownstreamRoute[0].DownstreamHeadersFindAndReplace.ShouldBe(_ht.Downstream); _result[routeIndex].DownstreamRoute[0].AddHeadersToUpstream.ShouldBe(_ht.AddHeadersToUpstream); _result[routeIndex].DownstreamRoute[0].AddHeadersToDownstream.ShouldBe(_ht.AddHeadersToDownstream); _result[routeIndex].DownstreamRoute[0].DownstreamAddresses.ShouldBe(_dhp); _result[routeIndex].DownstreamRoute[0].LoadBalancerOptions.ShouldBe(_lbo); _result[routeIndex].DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue(); _result[routeIndex].DownstreamRoute[0].DangerousAcceptAnyServerCertificateValidator.ShouldBe(expected.DangerousAcceptAnyServerCertificateValidator); _result[routeIndex].DownstreamRoute[0].DelegatingHandlers.ShouldBe(expected.DelegatingHandlers); _result[routeIndex].DownstreamRoute[0].ServiceName.ShouldBe(expected.ServiceName); _result[routeIndex].DownstreamRoute[0].DownstreamScheme.ShouldBe(expected.DownstreamScheme); _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); _result[routeIndex].DownstreamRoute[0].MetadataOptions.Metadata.ShouldBe(_expectedMetadata); _result[routeIndex].UpstreamHttpMethod.Count.ShouldBe(2); _result[routeIndex].UpstreamHttpMethod.ShouldAllBe(actual => expected.UpstreamHttpMethod.Contains(actual.Method)); _result[routeIndex].UpstreamHost.ShouldBe(expected.UpstreamHost); _result[routeIndex].DownstreamRoute.Count.ShouldBe(1); _result[routeIndex].UpstreamTemplatePattern.ShouldBe(_upt); _result[routeIndex].UpstreamHeaderTemplates.ShouldBe(_uht); } private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguration globalConfig) { _ridkCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _rrkCreator.Verify(x => x.Create(fileRoute, It.IsAny()), Times.Once); _utpCreator.Verify(x => x.Create(fileRoute), Times.Exactly(2)); _aoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _cthCreator.Verify(x => x.Create(fileRoute.AddHeadersToRequest), Times.Once); _cthCreator.Verify(x => x.Create(fileRoute.AddClaimsToRequest), Times.Once); _cthCreator.Verify(x => x.Create(fileRoute.AddQueriesToRequest), Times.Once); _qosoCreator.Verify(x => x.Create(fileRoute, globalConfig)); _rloCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _coCreator.Verify(x => x.Create(fileRoute, globalConfig, _rrk), Times.Once); _hhoCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _hfarCreator.Verify(x => x.Create(fileRoute), Times.Never); _hfarCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); _lboCreator.Verify(x => x.Create(fileRoute, globalConfig), Times.Once); _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions, globalConfig), Times.Once); _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/UpstreamHeaderTemplatePatternCreatorTests.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; public class UpstreamHeaderTemplatePatternCreatorTests { private readonly UpstreamHeaderTemplatePatternCreator _creator = new(); [Trait("PR", "1312")] [Trait("Feat", "360")] [Theory(DisplayName = "Should create pattern")] [InlineData("country", "a text without placeholders", "^(?i)a text without placeholders$", " without placeholders")] [InlineData("country", "a text without placeholders", "^a text without placeholders$", " Route is case sensitive", true)] [InlineData("country", "{header:start}rest of the text", "^(?i)(?.+)rest of the text$", " with placeholder in the beginning")] [InlineData("country", "rest of the text{header:end}", "^(?i)rest of the text(?.+)$", " with placeholder at the end")] [InlineData("country", "{header:countrycode}", "^(?i)(?.+)$", " with placeholder only")] [InlineData("country", "any text {header:cc} and other {header:version} and {header:bob} the end", "^(?i)any text (?.+) and other (?.+) and (?.+) the end$", " with more placeholders")] public void Create_WithUpstreamHeaderTemplates_ShouldCreatePattern(string key, string template, string expected, string withMessage, bool? isCaseSensitive = null) { // Arrange var fileRoute = new FileRoute { RouteIsCaseSensitive = isCaseSensitive ?? false, UpstreamHeaderTemplates = new Dictionary { [key] = template, }, }; // Act var actual = _creator.Create(fileRoute); // Assert var message = nameof(Create_WithUpstreamHeaderTemplates_ShouldCreatePattern).Replace('_', ' ') + withMessage; actual[key].ShouldNotBeNull() .Template.ShouldBe(expected, message); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/UpstreamTemplatePatternCreatorTests.cs ================================================ using Ocelot.Cache; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Values; using System; using System.Reflection; using System.Text.RegularExpressions; namespace Ocelot.UnitTests.Configuration; public class UpstreamTemplatePatternCreatorTests : UnitTest { private readonly Mock> _cache; private readonly UpstreamTemplatePatternCreator _creator; private const string MatchEverything = UpstreamTemplatePatternCreator.RegExMatchZeroOrMoreOfEverything; public UpstreamTemplatePatternCreatorTests() { _cache = new(); _creator = new UpstreamTemplatePatternCreator(_cache.Object); } [Fact] public void Should_match_up_to_next_slash() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/v{apiVersion}/cards", Priority = 0, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^(?i)/api/v[^/]+/cards$"); result.Priority.ShouldBe(0); } [Fact] public void Should_use_re_route_priority() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/orders/{catchAll}", Priority = 0, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^(?i)/orders(?:|/{MatchEverything})$"); result.Priority.ShouldBe(0); } [Fact] public void Should_use_zero_priority() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/{catchAll}", Priority = 1, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^/.*"); result.Priority.ShouldBe(0); } [Fact] public void Should_set_upstream_template_pattern_to_ignore_case_sensitivity() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/PRODUCTS/{productId}", RouteIsCaseSensitive = false, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^(?i)/PRODUCTS(?:|/{MatchEverything})$"); result.Priority.ShouldBe(1); } [Fact] public void Should_match_forward_slash_or_no_forward_slash_if_template_end_with_forward_slash() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/PRODUCTS/", RouteIsCaseSensitive = false, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^(?i)/PRODUCTS(/|)$"); result.Priority.ShouldBe(1); } [Fact] public void Should_set_upstream_template_pattern_to_respect_case_sensitivity() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/PRODUCTS/{productId}", RouteIsCaseSensitive = true, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^/PRODUCTS(?:|/{MatchEverything})$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_anything_to_end_of_string() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/products/{productId}", RouteIsCaseSensitive = true, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^/api/products(?:|/{MatchEverything})$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_more_than_one_placeholder() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}", RouteIsCaseSensitive = true, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^/api/products/[^/]+/variants(?:|/{MatchEverything})$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_more_than_one_placeholder_with_trailing_slash() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/products/{productId}/variants/{variantId}/", RouteIsCaseSensitive = true, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^/api/products/[^/]+/variants/[^/]+(/|)$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_to_end_of_string() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/", }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^/$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_to_end_of_string_when_slash_and_placeholder() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/{url}", }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^/.*"); result.Priority.ShouldBe(0); } [Fact] public void Should_create_template_pattern_that_starts_with_placeholder_then_has_another_later() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/{productId}/products/variants/{variantId}/", RouteIsCaseSensitive = true, }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe("^/[^/]+/products/variants/[^/]+(/|)$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_query_string() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($@"^(?i)/api/subscriptions/[^/]+/updates(/$|/\?|\?|$)unitId={MatchEverything}$"); result.Priority.ShouldBe(1); } [Fact] public void Should_create_template_pattern_that_matches_query_string_with_multiple_params() { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId={productId}", }; // Act var result = _creator.Create(fileRoute); // Assert result.Template.ShouldBe($"^(?i)/api/subscriptions/[^/]+/updates(/$|/\\?|\\?|$)unitId={MatchEverything}&productId={MatchEverything}$"); result.Priority.ShouldBe(1); } [Theory] [Trait("Bug", "2064")] [InlineData("/{tenantId}/products?{everything}", "/1/products/1", false)] [InlineData("/{tenantId}/products/{everything}", "/1/products/1", true)] public void Should_not_match_when_placeholder_appears_after_query_start(string urlPathTemplate, string requestPath, bool shouldMatch) { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = urlPathTemplate, }; // Act var result = _creator.Create(fileRoute); // Assert result.ShouldMatchWithRegex(requestPath, shouldMatch); } [Theory] [Trait("Bug", "2132")] [InlineData("/api/v1/abc?{everything}", "/api/v1/abc2/apple", false)] [InlineData("/api/v1/abc2/{everything}", "/api/v1/abc2/apple", true)] public void Should_not_match_with_query_param_wildcard(string urlPathTemplate, string requestPath, bool shouldMatch) { // Arrange var fileRoute = new FileRoute { UpstreamPathTemplate = urlPathTemplate, }; // Act var result = _creator.Create(fileRoute); // Assert result.ShouldMatchWithRegex(requestPath, shouldMatch); } [Fact] [Trait("Feat", "1348")] [Trait("Bug", "2246")] public void GetRegex_NoKey_ReturnsNull() { // Act var actual = GetRegex.Invoke(_creator, new object[] { string.Empty }); // Assert actual.ShouldBeNull(); } [Fact] [Trait("Feat", "1348")] [Trait("Bug", "2246")] public void CreateTemplate_PatternProperty_NullChecks() { // Act string nullTemplate = null; var actual = CreateTemplate.Invoke(_creator, new object[] { nullTemplate, 0, false, null }) as UpstreamPathTemplate; // Assert actual.ShouldNotBeNull(); actual.Pattern.ShouldNotBeNull().ToString().ShouldBe("$^"); } private static Type Me { get; } = typeof(UpstreamTemplatePatternCreator); private static MethodInfo GetRegex { get; } = Me.GetMethod(nameof(GetRegex), BindingFlags.NonPublic | BindingFlags.Instance); private static MethodInfo CreateTemplate { get; } = Me.GetMethod(nameof(CreateTemplate), BindingFlags.NonPublic | BindingFlags.Instance); } internal static class UpstreamPathTemplateExtensions { public static void ShouldMatchWithRegex(this UpstreamPathTemplate actual, string requestPath, bool shouldMatch) { var match = Regex.Match(requestPath, actual.Template); Assert.Equal(shouldMatch, match.Success); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Validation/FileAuthenticationOptionsValidatorTests.cs ================================================ using FluentValidation.Results; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; namespace Ocelot.UnitTests.Configuration.Validation; [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 public class FileAuthenticationOptionsValidatorTests : UnitTest { private readonly FileAuthenticationOptionsValidator _validator; private readonly Mock _authProvider; private FileAuthenticationOptions _authenticationOptions; private ValidationResult _result; public FileAuthenticationOptionsValidatorTests() { _authProvider = new(); _validator = new(_authProvider.Object); } [Fact] public async Task Should_be_valid_if_specified_authentication_provider_is_registered() { // Arrange const string key = "JwtLads"; CreateAuthenticationOptions(key); GivenAnAuthProvider(key); // Act await ValidateAsync(); // Assert _result.IsValid.ShouldBeTrue(); } [Fact] public async Task Should_not_be_valid_if_specified_authentication_provider_is_not_registered() { // Arrange const string key = "JwtLads"; CreateAuthenticationOptions(key); // Act await ValidateAsync(); // Assert _result.IsValid.ShouldBeFalse(); _result.Errors[0].ErrorMessage.ShouldBe("AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'',AuthenticationProviderKeys:['JwtLads'] is unsupported authentication provider"); } private void GivenAnAuthProvider(string key) { var schemes = new List { new(key, key, typeof(FakeAuthHandler)), }; _authProvider.Setup(x => x.GetAllSchemesAsync()) .ReturnsAsync(schemes); } private void CreateAuthenticationOptions(string key) { _authenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKeys = [ key ], }; } private async Task ValidateAsync() { _result = await _validator.ValidateAsync(_authenticationOptions); } private class FakeAuthHandler : IAuthenticationHandler { public Task AuthenticateAsync() => throw new NotImplementedException(); public Task ChallengeAsync(AuthenticationProperties properties) => throw new NotImplementedException(); public Task ForbidAsync(AuthenticationProperties properties) => throw new NotImplementedException(); public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs ================================================ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using Ocelot.Logging; using Ocelot.QualityOfService; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using Ocelot.UnitTests.Requester; using Ocelot.Values; using System.Security.Claims; using System.Text.Encodings.Web; namespace Ocelot.UnitTests.Configuration.Validation; public class FileConfigurationFluentValidatorTests : UnitTest { private FileConfigurationFluentValidator _configurationValidator; private FileConfiguration _fileConfiguration; private Response _result; private IServiceProvider _provider; private readonly ServiceCollection _services; private readonly Mock _authProvider; private readonly FileAuthenticationOptionsValidator _fileAuthOptsValidator; public FileConfigurationFluentValidatorTests() { _services = new ServiceCollection(); _authProvider = new Mock(); _fileAuthOptsValidator = new FileAuthenticationOptionsValidator(_authProvider.Object); _provider = _services.BuildServiceProvider(true); // TODO Replace with mocks _configurationValidator = new FileConfigurationFluentValidator( _provider, new(new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_provider), _fileAuthOptsValidator), new(new FileQoSOptionsFluentValidator(_provider), _fileAuthOptsValidator)); } [Fact] public async Task Configuration_is_valid_if_service_discovery_options_specified_and_has_service_fabric_as_option() { // Arrange var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_if_service_discovery_options_specified_and_has_service_discovery_handler() { // Arrange var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = "FakeServiceDiscoveryProvider"; GivenAConfiguration(configuration); GivenAServiceDiscoveryHandler(); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_if_service_discovery_options_specified_dynamically_and_has_service_discovery_handler() { // Arrange var configuration = new FileConfiguration(); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = "FakeServiceDiscoveryProvider"; GivenAConfiguration(configuration); GivenAServiceDiscoveryHandler(); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler() { // Arrange var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = "FakeServiceDiscoveryProvider"; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); } [Fact] public async Task Configuration_is_invalid_if_service_discovery_options_specified_dynamically_but_service_discovery_handler() { // Arrange var configuration = new FileConfiguration(); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = "FakeServiceDiscoveryProvider"; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); } [Fact] public async Task Configuration_is_invalid_if_service_discovery_options_specified_but_no_service_discovery_handler_with_matching_name() { // Arrange var route = GivenServiceDiscoveryRoute(); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); configuration.GlobalConfiguration.ServiceDiscoveryProvider.Type = "consul"; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); GivenAServiceDiscoveryHandler(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot, errors are: Unable to start Ocelot because either a Route or GlobalConfiguration are using ServiceDiscoveryOptions but no ServiceDiscoveryFinderDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Consul and services.AddConsul() or Ocelot.Provider.Eureka and services.AddEureka()?"); } [Fact] public async Task Configuration_is_valid_if_qos_options_specified_and_has_qos_handler() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); route.QoSOptions = new FileQoSOptions { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; GivenAConfiguration(route); GivenAQoSHandler(); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_if_qos_options_specified_globally_and_has_qos_handler() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; GivenAConfiguration(configuration); GivenAQoSHandler(); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_if_qos_options_specified_but_no_qos_handler() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); route.QoSOptions = new FileQoSOptions { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); } [Fact] public async Task Configuration_is_invalid_if_qos_options_specified_globally_but_no_qos_handler() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var configuration = GivenAConfiguration(route); configuration.GlobalConfiguration.QoSOptions = new() { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); } [Fact] public async Task Configuration_is_valid_if_aggregates_are_valid() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var route2 = GivenDefaultRoute("/tom", "/", key: "Tom"); var configuration = GivenAConfiguration(route, route2); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_if_aggregates_are_duplicate_of_routes() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var route2 = GivenDefaultRoute("/tom", "/", key: "Tom", upstreamHost: "localhost"); var configuration = GivenAConfiguration(route, route2); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "route /tom has duplicate aggregate"); } [Fact] public async Task Configuration_is_valid_if_aggregates_are_not_duplicate_of_routes() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var route2 = GivenDefaultRoute("/tom", "/", key: "Tom"); route2.UpstreamHttpMethod.Add("Post"); var configuration = GivenAConfiguration(route, route2); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_if_aggregates_are_duplicate_of_aggregates() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var route2 = GivenDefaultRoute("/lol", "/", key: "Tom"); var configuration = GivenAConfiguration(route, route2); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, new() { UpstreamPathTemplate = "/tom", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "aggregate /tom has duplicate aggregate"); } [Fact] public async Task Configuration_is_invalid_if_routes_dont_exist_for_aggregate() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var configuration = GivenAConfiguration(route); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "Routes for aggregateRoute / either do not exist or do not have correct ServiceName property"); } [Fact] public async Task Configuration_is_invalid_if_aggregate_has_routes_with_specific_request_id_keys() { // Arrange var route = GivenDefaultRoute("/laura", "/", key: "Laura"); var route2 = GivenDefaultRoute("/tom", "/", key: "Tom"); route2.RequestIdKey = "should_fail"; var configuration = GivenAConfiguration(route, route2); configuration.Aggregates = new() { new() { UpstreamPathTemplate = "/", UpstreamHost = "localhost", RouteKeys = ["Tom", "Laura"], }, }; GivenAConfiguration(configuration); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "aggregateRoute / contains Route with specific RequestIdKey, this is not possible with Aggregates"); } [Fact] public async Task Configuration_is_invalid_if_scheme_in_downstream_or_upstream_template() { // Arrange GivenAConfiguration(GivenDefaultRoute("http://asdf.com", "http://www.bbc.co.uk/api/products/{productId}")); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorIs(); ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} doesnt start with forward slash"); ThenTheErrorMessageAtPositionIs(1, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); ThenTheErrorMessageAtPositionIs(2, "Downstream Path Template http://www.bbc.co.uk/api/products/{productId} contains scheme"); ThenTheErrorMessageAtPositionIs(3, "Upstream Path Template http://asdf.com contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); ThenTheErrorMessageAtPositionIs(4, "Upstream Path Template http://asdf.com doesnt start with forward slash"); ThenTheErrorMessageAtPositionIs(5, "Upstream Path Template http://asdf.com contains scheme"); } [Fact] public async Task Configuration_is_valid_with_one_route() { // Arrange GivenAConfiguration(GivenDefaultRoute()); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_without_slash_prefix_downstream_path_template() { // Arrange GivenAConfiguration(GivenDefaultRoute("/asdf/", "api/products/")); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template api/products/ doesnt start with forward slash"); } [Fact] public async Task Configuration_is_invalid_without_slash_prefix_upstream_path_template() { // Arrange GivenAConfiguration(GivenDefaultRoute("api/prod/", "/api/products/")); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template api/prod/ doesnt start with forward slash"); } [Fact] public async Task Configuration_is_invalid_if_upstream_url_contains_forward_slash_then_another_forward_slash() { // Arrange GivenAConfiguration(GivenDefaultRoute("//api/prod/", "/api/products/")); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "Upstream Path Template //api/prod/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Fact] public async Task Configuration_is_invalid_if_downstream_url_contains_forward_slash_then_another_forward_slash() { // Arrange GivenAConfiguration(GivenDefaultRoute("/api/prod/", "//api/products/")); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "Downstream Path Template //api/products/ contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Fact] public async Task Configuration_is_valid_with_valid_authentication_provider() { // Arrange var route = GivenDefaultRoute(); route.AuthenticationOptions = new() { AuthenticationProviderKey = "Test" }; GivenAConfiguration(route); GivenTheAuthSchemeExists("Test"); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_with_invalid_authentication_provider() { // Arrange var route = GivenDefaultRoute(); route.AuthenticationOptions = new FileAuthenticationOptions() { AuthenticationProviderKey = "Test", AuthenticationProviderKeys = new string[] { "Test #1", "Test #2" }, }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'Test',AuthenticationProviderKeys:['Test #1','Test #2'] is unsupported authentication provider"); } [Fact] public async Task Configuration_is_not_valid_with_duplicate_routes_all_verbs() { // Arrange var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(null, "/www/test/"); GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); } [Fact] public async Task Configuration_is_valid_with_duplicate_routes_all_verbs_but_different_hosts() { // Arrange var route = GivenDefaultRoute(null, null, upstreamHost: "host1"); var duplicate = GivenDefaultRoute(null, "/www/test/", upstreamHost: "host2"); GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_not_valid_with_duplicate_routes_specific_verbs() { // Arrange var route = GivenDefaultRoute(); var duplicate = GivenDefaultRoute(null, "/www/test/"); duplicate.UpstreamHttpMethod = [HttpMethods.Get]; GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); } [Fact] public async Task Configuration_is_valid_with_duplicate_routes_different_verbs() { // Arrange var route = GivenDefaultRoute(); // "Get" verb is inside var duplicate = GivenDefaultRoute(null, "/www/test/"); duplicate.UpstreamHttpMethod = [HttpMethods.Post]; GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_not_valid_with_duplicate_routes_with_duplicated_upstreamhosts() { // Arrange var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: "upstreamhost"); var duplicate = GivenDefaultRoute(null, "/www/test/", methods: [], upstreamHost: "upstreamhost"); GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); } [Fact] public async Task Configuration_is_valid_with_duplicate_routes_but_different_upstreamhosts() { // Arrange var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: "upstreamhost111"); var duplicate = GivenDefaultRoute(null, "/www/test/", methods: [], upstreamHost: "upstreamhost222"); GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_with_duplicate_routes_but_one_upstreamhost_is_not_set() { // Arrange var route = GivenDefaultRoute(null, null, methods: [], upstreamHost: "upstreamhost"); var duplicate = GivenDefaultRoute(null, "/www/test/", methods: [], upstreamHost: null); // ! GivenAConfiguration(route, duplicate); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_invalid_with_invalid_rate_limit_configuration() { // Arrange var route = GivenDefaultRoute(); route.RateLimitOptions = new FileRateLimitByHeaderRule { Period = "1x", EnableRateLimiting = true, }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); } [Fact] public async Task Configuration_is_valid_with_valid_rate_limit_configuration() { // Arrange var route = GivenDefaultRoute(); route.RateLimitOptions = new FileRateLimitByHeaderRule { Period = "1d", EnableRateLimiting = true, }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_with_using_service_discovery_and_service_name() { // Arrange var route = GivenServiceDiscoveryRoute(); var config = GivenAConfiguration(route); config.GlobalConfiguration.ServiceDiscoveryProvider = GivenDefaultServiceDiscoveryProvider(); GivenAConfiguration(config); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } private const string Empty = ""; [Theory] [InlineData(null)] [InlineData(Empty)] public async Task Configuration_is_invalid_when_not_using_service_discovery_and_host(string downstreamHost) { // Arrange var route = GivenDefaultRoute(); route.DownstreamHostAndPorts[0].Host = downstreamHost; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); } [Theory] [InlineData(null, true)] [InlineData(Empty, true)] [InlineData("Test", false)] public async Task HaveServiceDiscoveryProviderRegistered_RouteServiceName_Validated(string serviceName, bool valid) { // Arrange var route = GivenDefaultRoute(); route.ServiceName = serviceName; var config = GivenAConfiguration(route); config.GlobalConfiguration.ServiceDiscoveryProvider = null; // Act await WhenIValidateTheConfiguration(); // Assert _result.Data.IsError.ShouldNotBe(valid); _result.Data.Errors.Count.ShouldBe(valid ? 0 : 1); } [Theory] [InlineData(false, null, false)] [InlineData(true, null, false)] [InlineData(true, "type", false)] [InlineData(true, "servicefabric", true)] public async Task HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryProvider_Validated(bool create, string type, bool valid) { // Arrange var route = GivenServiceDiscoveryRoute(); var config = GivenAConfiguration(route); var provider = create ? GivenDefaultServiceDiscoveryProvider() : null; config.GlobalConfiguration.ServiceDiscoveryProvider = provider; if (create && provider != null) { provider.Type = type; } // Act await WhenIValidateTheConfiguration(); // Assert _result.Data.IsError.ShouldNotBe(valid); _result.Data.Errors.Count.ShouldBeGreaterThanOrEqualTo(valid ? 0 : 1); } [Theory] [InlineData(false)] [InlineData(true)] public async Task HaveServiceDiscoveryProviderRegistered_ServiceDiscoveryFinderDelegates_Validated(bool hasDelegate) { // Arrange var valid = hasDelegate; var route = GivenServiceDiscoveryRoute(); var config = GivenAConfiguration(route); config.GlobalConfiguration.ServiceDiscoveryProvider = null; if (hasDelegate) { GivenAServiceDiscoveryHandler(); } // Act await WhenIValidateTheConfiguration(); // Assert _result.Data.IsError.ShouldNotBe(valid); _result.Data.Errors.Count.ShouldBe(valid ? 0 : 1); } [Fact] public async Task Configuration_is_valid_when_not_using_service_discovery_and_host_is_set() { // Arrange var route = GivenDefaultRoute(); route.DownstreamHostAndPorts = new() { new("bbc.co.uk", 123), }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_valid_when_no_downstream_but_has_host_and_port() { // Arrange var route = GivenDefaultRoute(); route.DownstreamHostAndPorts = new() { new("test", 123), }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] public async Task Configuration_is_not_valid_when_no_host_and_port() { // Arrange var route = GivenDefaultRoute(); route.DownstreamHostAndPorts = new(); GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); } [Fact] public async Task Configuration_is_not_valid_when_host_and_port_is_empty() { // Arrange var route = GivenDefaultRoute(); route.DownstreamHostAndPorts = new() { new(), }; GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public async Task Configuration_is_not_valid_when_upstream_headers_the_same() { // Arrange var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() { { "header1", "value1" }, { "header2", "value2" }, }); var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() { { "header2", "value2" }, { "header1", "value1" }, }); GivenAConfiguration(route1, route2); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "route /asdf/ has duplicate"); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public async Task Configuration_is_valid_when_upstream_headers_not_the_same() { // Arrange var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() { { "header1", "value1" }, { "header2", "value2" }, }); var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() { { "header2", "value2" }, { "header1", "valueDIFFERENT" }, }); GivenAConfiguration(route1, route2); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public async Task Configuration_is_valid_when_upstream_headers_count_not_the_same() { // Arrange var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() { { "header1", "value1" }, { "header2", "value2" }, }); var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new() { { "header2", "value2" }, }); GivenAConfiguration(route1, route2); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public async Task Configuration_is_valid_when_one_upstream_headers_empty_and_other_not_empty() { // Arrange var route1 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/api/products/", new() { { "header1", "value1" }, { "header2", "value2" }, }); var route2 = GivenRouteWithUpstreamHeaderTemplates("/asdf/", "/www/test/", new()); GivenAConfiguration(route1, route2); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsValid(); } [Theory] [Trait("PR", "1927")] [InlineData("/foo/{bar}/foo", "/yahoo/foo/{bar}")] // valid [InlineData("/foo/{bar}/{foo}", "/yahoo/{foo}/{bar}")] // valid [InlineData("/foo/{bar}/{bar}", "/yahoo/foo/{bar}", "UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid [InlineData("/foo/{bar}/{bar}", "/yahoo/{foo}/{bar}", "UpstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid [InlineData("/yahoo/foo/{bar}", "/foo/{bar}/foo")] // valid [InlineData("/yahoo/{foo}/{bar}", "/foo/{bar}/{foo}")] // valid [InlineData("/yahoo/foo/{bar}", "/foo/{bar}/{bar}", "DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid [InlineData("/yahoo/{foo}/{bar}", "/foo/{bar}/{bar}", "DownstreamPathTemplate '/foo/{bar}/{bar}' has duplicated placeholder")] // invalid public async Task IsPlaceholderNotDuplicatedIn_RuleForFileRoute_PathTemplatePlaceholdersAreValidated(string upstream, string downstream, params string[] expected) { // Arrange var route = GivenDefaultRoute(upstream, downstream); GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenThereAreErrors(expected.Length > 0); ThenTheErrorMessagesAre(expected); } [Theory] [Trait("PR", "1927")] [Trait("Bug", "683")] [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder")] [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] public async Task Configuration_is_invalid_when_placeholder_is_used_twice_in_upstream_path_template(string upstream, string downstream, string host, params string[] expected) { // Arrange var route = GivenDefaultRoute(upstream, downstream, upstreamHost: host); GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessagesAre(expected); } [Theory] [Trait("PR", "1927")] [Trait("Bug", "683")] [InlineData("/foo/bar/{everything}", "/bar/{everything}/{everything}", "foo", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] [InlineData("/foo/bar/{everything}/{everything}", "/bar/{everything}/{everything}", "foo", "UpstreamPathTemplate '/foo/bar/{everything}/{everything}' has duplicated placeholder", "DownstreamPathTemplate '/bar/{everything}/{everything}' has duplicated placeholder")] public async Task Configuration_is_invalid_when_placeholder_is_used_twice_in_downstream_path_template(string upstream, string downstream, string host, params string[] expected) { // Arrange var route = GivenDefaultRoute(upstream, downstream, upstreamHost: host); GivenAConfiguration(route); // Act await WhenIValidateTheConfiguration(); // Assert ThenTheResultIsNotValid(); ThenTheErrorMessagesAre(expected); } #region PR 2114 [Fact] [Trait("PR", "2114")] // https://github.com/ThreeMammals/Ocelot/pull/2114 [Trait("Feat", "842")] // https://github.com/ThreeMammals/Ocelot/issues/842 public async Task Configuration_is_not_valid_if_specified_authentication_provider_is_not_registered() { const string key = "JwtLads"; GivenConfigurationWithAuthenticationKey(key); await WhenIValidateTheConfiguration(); ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[] is unsupported authentication provider"); } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public async Task Configuration_is_valid_if_specified_authentication_provider_is_registered() { const string key = "JwtLads"; GivenConfigurationWithAuthenticationKey(key); GivenTheAuthSchemeExists(key); await WhenIValidateTheConfiguration(); ThenTheResultIsValid(); } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public async Task Configuration_is_not_valid_if_one_authentication_provider_is_not_registered() { string[] keys = { "JwtLads", "other" }; GivenConfigurationWithAuthenticationKeys(keys); await WhenIValidateTheConfiguration(); ThenTheResultIsNotValid(); ThenTheErrorMessageAtPositionIs(0, "AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'',AuthenticationProviderKeys:['JwtLads','other'] is unsupported authentication provider"); } [Fact] [Trait("PR", "2114")] [Trait("Feat", "842")] public async Task Configuration_is_valid_if_all_specified_authentication_provider_are_registered() { string[] keys = { "JwtLads", "other" }; GivenConfigurationWithAuthenticationKeys(keys); GivenTheAuthSchemesExists(keys); await WhenIValidateTheConfiguration(); ThenTheResultIsValid(); } #endregion private static FileRoute GivenDefaultRoute() => GivenDefaultRoute(null, null); private static FileRoute GivenDefaultRoute(string upstream, string downstream, string key = null, string[] methods = null, string upstreamHost = null) => new() { UpstreamHost = upstreamHost, UpstreamHttpMethod = methods is null || methods.Length == 0 ? [HttpMethods.Get] : [..methods], UpstreamPathTemplate = upstream ?? "/asdf/", DownstreamPathTemplate = downstream ?? "/api/products/", DownstreamHostAndPorts = new() { new("bbc.co.uk", 12345), }, DownstreamScheme = Uri.UriSchemeHttp, Key = key, }; private static FileRoute GivenServiceDiscoveryRoute() => new() { UpstreamHttpMethod = [HttpMethods.Get], UpstreamPathTemplate = "/laura", DownstreamPathTemplate = "/", DownstreamScheme = Uri.UriSchemeHttp, ServiceName = "test", }; private static FileRoute GivenRouteWithUpstreamHeaderTemplates(string upstream, string downstream, Dictionary templates) => new() { UpstreamPathTemplate = upstream, DownstreamPathTemplate = downstream, DownstreamHostAndPorts = new() { new("bbc.co.uk", 123), }, UpstreamHttpMethod = [HttpMethods.Get], UpstreamHeaderTemplates = templates, }; private void GivenAConfiguration(FileConfiguration fileConfiguration) => _fileConfiguration = fileConfiguration; private FileConfiguration GivenAConfiguration(params FileRoute[] routes) { var config = new FileConfiguration(); config.Routes.AddRange(routes); _fileConfiguration = config; return config; } private void GivenConfigurationWithAuthenticationKey(string key) { _fileConfiguration = new FileConfiguration(); _fileConfiguration.GlobalConfiguration.AuthenticationOptions.AuthenticationProviderKey = key; } private void GivenConfigurationWithAuthenticationKeys(string[] keys) { _fileConfiguration = new FileConfiguration(); _fileConfiguration.GlobalConfiguration.AuthenticationOptions.AuthenticationProviderKeys = keys; } private static FileServiceDiscoveryProvider GivenDefaultServiceDiscoveryProvider() => new() { Scheme = Uri.UriSchemeHttps, Host = "localhost", Type = "ServiceFabric", Port = 8500, }; private async Task WhenIValidateTheConfiguration() => _result = await _configurationValidator.IsValid(_fileConfiguration); private void ThenTheResultIsValid() => _result.Data.IsError.ShouldBeFalse(); private void ThenTheResultIsNotValid() => _result.Data.IsError.ShouldBeTrue(); private void ThenTheErrorIs() => _result.Data.Errors[0].ShouldBeOfType(); private void ThenTheErrorMessageAtPositionIs(int index, string expected) => _result.Data.Errors[index].Message.ShouldBe(expected); private void ThenThereAreErrors(bool isError) => _result.Data.IsError.ShouldBe(isError); private void ThenTheErrorMessagesAre(IEnumerable messages) { _result.Data.Errors.Count.ShouldBe(messages.Count()); foreach (var msg in messages) { _result.Data.Errors.ShouldContain(e => e.Message == msg); } } private void GivenTheAuthSchemeExists(string name) { _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(new List { new(name, name, typeof(TestHandler)), }); } private void GivenTheAuthSchemesExists(string[] names) { _authProvider.Setup(x => x.GetAllSchemesAsync()).ReturnsAsync(names.Select(n => new AuthenticationScheme(n, n, typeof(TestHandler)))); } private void GivenAQoSHandler() { static DelegatingHandler Del(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); _services.AddSingleton((QosDelegatingHandlerDelegate)Del); _provider = _services.BuildServiceProvider(true); _configurationValidator = new FileConfigurationFluentValidator( _provider, new(new(), new(_provider), _fileAuthOptsValidator), new(new(_provider), _fileAuthOptsValidator)); } private void GivenAServiceDiscoveryHandler() { static IServiceDiscoveryProvider del(IServiceProvider a, ServiceProviderConfiguration b, DownstreamRoute c) => new FakeServiceDiscoveryProvider(); _services.AddSingleton((ServiceDiscoveryFinderDelegate)del); _provider = _services.BuildServiceProvider(true); _configurationValidator = new FileConfigurationFluentValidator( _provider, new(new(), new(_provider), _fileAuthOptsValidator), new(new(_provider), _fileAuthOptsValidator)); } private class FakeServiceDiscoveryProvider : IServiceDiscoveryProvider { public Task> GetAsync() => Task.FromResult>(new()); } private class TestOptions : AuthenticationSchemeOptions { } private class TestHandler : AuthenticationHandler { // https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/8.0/isystemclock-obsolete // .NET 8.0: TimeProvider is now a settable property on the Options classes for the authentication and identity components. // It can be set directly or by registering a provider in the dependency injection container. public TestHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) { } protected override Task HandleAuthenticateAsync() { var principal = new ClaimsPrincipal(); return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name))); } } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using Ocelot.Logging; using Ocelot.QualityOfService; namespace Ocelot.UnitTests.Configuration.Validation; public class FileQoSOptionsFluentValidatorTests : UnitTest { private FileQoSOptionsFluentValidator _validator; private readonly ServiceCollection _services; public FileQoSOptionsFluentValidatorTests() { _services = new ServiceCollection(); var provider = _services.BuildServiceProvider(true); _validator = new FileQoSOptionsFluentValidator(provider); } [Fact] public void Should_be_valid_as_nothing_set() { // Arrange var qosOptions = new FileQoSOptions(); // Act var result = _validator.Validate(qosOptions); // Assert result.IsValid.ShouldBeTrue(); } [Fact] public void Should_be_valid_as_qos_delegate_set() { // Arrange var qosOptions = new FileQoSOptions { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; GivenAQosDelegate(); // Act var result = _validator.Validate(qosOptions); // Assert result.IsValid.ShouldBeTrue(); } [Fact] public void Should_be_invalid_as_no_qos_delegate() { // Arrange var qosOptions = new FileQoSOptions { TimeoutValue = 1, ExceptionsAllowedBeforeBreaking = 1, }; // Act var result = _validator.Validate(qosOptions); // Assert result.IsValid.ShouldBeFalse(); result.Errors[0].ErrorMessage.ShouldBe("Unable to start Ocelot because either a Route or GlobalConfiguration are using QoSOptions but no QosDelegatingHandlerDelegate has been registered in dependency injection container. Are you missing a package like Ocelot.Provider.Polly and services.AddPolly()?"); } private void GivenAQosDelegate() { static DelegatingHandler Fake(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => null; _services.AddSingleton((QosDelegatingHandlerDelegate)Fake); var provider = _services.BuildServiceProvider(true); _validator = new FileQoSOptionsFluentValidator(provider); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Validation/HostAndPortValidatorTests.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; namespace Ocelot.UnitTests.Configuration.Validation; public class HostAndPortValidatorTests : UnitTest { private readonly HostAndPortValidator _validator; public HostAndPortValidatorTests() { _validator = new HostAndPortValidator(); } [Theory] [InlineData(null)] [InlineData("")] public void Should_be_invalid_because_host_empty(string host) { // Arrange var hostAndPort = new FileHostAndPort { Host = host, }; // Act var result = _validator.Validate(hostAndPort); // Assert result.IsValid.ShouldBeFalse(); result.Errors[0].ErrorMessage.ShouldBe("When not using service discovery Host must be set on DownstreamHostAndPorts if you are not using Route.Host or Ocelot cannot find your service!"); } [Fact] public void Should_be_valid_because_host_set() { // Arrange var hostAndPort = new FileHostAndPort { Host = "test", }; // Act var result = _validator.Validate(hostAndPort); // Assert result.IsValid.ShouldBeTrue(); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs ================================================ using FluentValidation.Results; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; using System.Reflection; namespace Ocelot.UnitTests.Configuration.Validation; public class RouteFluentValidatorTests : UnitTest { private readonly RouteFluentValidator _validator; private readonly Mock _authProvider; private readonly Mock _serviceProvider; public RouteFluentValidatorTests() { _authProvider = new Mock(); _serviceProvider = new Mock(); // TODO - replace with mocks _validator = new RouteFluentValidator( new HostAndPortValidator(), new FileQoSOptionsFluentValidator(_serviceProvider.Object), new FileAuthenticationOptionsValidator(_authProvider.Object)); } [Fact] public async Task Downstream_path_template_should_not_be_empty() { // Arrange var route = new FileRoute(); // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Downstream Path Template cannot be empty"); } [Fact] public async Task Upstream_path_template_should_not_be_empty() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Upstream Path Template cannot be empty"); } [Fact] public async Task Downstream_path_template_should_start_with_forward_slash() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Downstream Path Template test doesnt start with forward slash"); } [Fact] public async Task Downstream_path_template_should_not_contain_double_forward_slash() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "//test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Downstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Theory] [InlineData("https://test")] [InlineData("http://test")] [InlineData("/test/http://")] [InlineData("/test/https://")] public async Task Downstream_path_template_should_not_contain_scheme(string downstreamPathTemplate) { // Arrange var route = new FileRoute { DownstreamPathTemplate = downstreamPathTemplate, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains($"Downstream Path Template {downstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Fact] public async Task Upstream_path_template_should_start_with_forward_slash() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Upstream Path Template test doesnt start with forward slash"); } [Fact] public async Task Upstream_path_template_should_not_contain_double_forward_slash() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "//test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("Upstream Path Template //test contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Theory] [InlineData("https://test")] [InlineData("http://test")] [InlineData("/test/http://")] [InlineData("/test/https://")] public async Task Upstream_path_template_should_not_contain_scheme(string upstreamPathTemplate) { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = upstreamPathTemplate, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains($"Upstream Path Template {upstreamPathTemplate} contains double forward slash, Ocelot does not support this at the moment. Please raise an issue in GitHib if you need this feature."); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, true, null)] [InlineData(-1L, false, "RateLimitOptions.Limit is negative or zero for the route /test")] [InlineData(0L, false, "RateLimitOptions.Limit is negative or zero for the route /test")] [InlineData(1L, true, null)] public async Task ShouldValidate_FileRateLimitByHeaderRule_Limit(long? limit, bool valid, string errorMessage) { // Arrange var route = GivenRoute(); route.RateLimitOptions = new() { Limit = limit, Period = "1s", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBe(valid); if (!result.IsValid) result.ThenSingleErrorIs(errorMessage); } [Fact] public async Task Should_not_be_valid_if_enable_rate_limiting_true_and_period_is_empty() { // Arrange var route = GivenRoute(); route.RateLimitOptions = new() { EnableRateLimiting = true, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.ThenTheErrorsContains("RateLimitOptions.Period is empty"); result.ThenTheErrorsContains("RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); } [Fact] public async Task Should_not_be_valid_if_enable_rate_limiting_true_and_period_has_value() { // Arrange var route = GivenRoute(); route.RateLimitOptions = new() { Period = "test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.ThenSingleErrorIs("RateLimitOptions.Period does not contain integer then ms (millisecond), s (second), m (minute), h (hour), d (day) e.g. 1m for 1 minute period"); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, false)] [InlineData("", false)] [InlineData("1s", true)] [InlineData("12.34s", true)] [InlineData("1ms", true)] [InlineData("12.34ms", true)] [InlineData("2m", true)] [InlineData("23.45m", true)] [InlineData("3h", true)] [InlineData("34.56h", true)] [InlineData("4d", true)] [InlineData("4.5d", true)] [InlineData("123", false)] [InlineData("-123", false)] [InlineData("bad", false)] [InlineData(" 3s ", true)] [InlineData(" -3s ", false)] public void IsValidPeriod_ReflectionLifeHack_BranchesAreCovered(string period, bool expected) { // Arrange var method = _validator.GetType().GetMethod("IsValidPeriod", BindingFlags.NonPublic | BindingFlags.Static); var argument = new FileRateLimitByHeaderRule { Period = period }; // Act bool actual = (bool)method.Invoke(_validator, new object[] { argument }); // Assert Assert.Equal(expected, actual); } [Fact] public async Task Should_not_be_valid_if_specified_authentication_provider_isnt_registered() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = "JwtLads", }, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("AuthenticationOptions: AllowAnonymous:False,AllowedScopes:[],AuthenticationProviderKey:'JwtLads',AuthenticationProviderKeys:[] is unsupported authentication provider"); } [Fact] public async Task Should_not_be_valid_if_not_using_service_discovery_and_no_host_and_ports() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("When not using service discovery DownstreamHostAndPorts must be set and not empty or Ocelot cannot find your service!"); } [Fact] public async Task Should_be_valid_if_using_service_discovery_and_no_host_and_ports() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", ServiceName = "Lads", }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeTrue(); } [Fact] public async Task Should_be_valid_re_route_using_host_and_port_and_paths() { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = 5000, }, }, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeTrue(); } [Fact] public async Task Should_be_valid_if_specified_authentication_provider_is_registered() { // Arrange const string key = "JwtLads"; var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", AuthenticationOptions = new FileAuthenticationOptions { AuthenticationProviderKey = key, }, DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = 5000, }, }, }; GivenAnAuthProvider(key); // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeTrue(); } [Theory] [InlineData("1.0")] [InlineData("1.1")] [InlineData("2.0")] [InlineData("1,0")] [InlineData("1,1")] [InlineData("2,0")] [InlineData("1")] [InlineData("2")] [InlineData("")] [InlineData(null)] public async Task Should_be_valid_re_route_using_downstream_http_version(string version) { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = 5000, }, }, DownstreamHttpVersion = version, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeTrue(); } [Theory] [InlineData("retg1.1")] [InlineData("re2.0")] [InlineData("1,0a")] [InlineData("a1,1")] [InlineData("12,0")] [InlineData("asdf")] public async Task Should_be_invalid_re_route_using_downstream_http_version(string version) { // Arrange var route = new FileRoute { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = 5000, }, }, DownstreamHttpVersion = version, }; // Act var result = await _validator.ValidateAsync(route, TestContext.Current.CancellationToken); // Assert result.IsValid.ShouldBeFalse(); result.ThenTheErrorsContains("'Downstream Http Version'"); // this error message changes depending on the OS language } private static FileRoute GivenRoute() => new() { DownstreamPathTemplate = "/test", UpstreamPathTemplate = "/test", DownstreamHostAndPorts = new() { new("localhost", 5000), }, }; private void GivenAnAuthProvider(string key) { var schemes = new List { new(key, key, typeof(FakeAutheHandler)), }; _authProvider .Setup(x => x.GetAllSchemesAsync()) .ReturnsAsync(schemes); } private class FakeAutheHandler : IAuthenticationHandler { public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context) { throw new System.NotImplementedException(); } public Task AuthenticateAsync() { throw new System.NotImplementedException(); } public Task ChallengeAsync(AuthenticationProperties properties) { throw new System.NotImplementedException(); } public Task ForbidAsync(AuthenticationProperties properties) { throw new System.NotImplementedException(); } } } static class ValidationResultExtensions { public static void ThenTheErrorsContains(this ValidationResult result, string expected) => result.Errors.ShouldContain(x => x.ErrorMessage.Contains(expected)); public static void ThenSingleErrorIs(this ValidationResult result, string message) { Assert.False(result.IsValid); Assert.Single(result.Errors); Assert.Equal(message, result.Errors[0].ErrorMessage); } } ================================================ FILE: test/Ocelot.UnitTests/Configuration/VersionCreatorTests.cs ================================================ using Ocelot.Configuration.Creator; namespace Ocelot.UnitTests.Configuration; public class VersionCreatorTests : UnitTest { private readonly HttpVersionCreator _creator = new(); [Fact] public void Should_create_version_based_on_input() { // Arrange, Act var result = _creator.Create("2.0"); // Assert result.Major.ShouldBe(2); result.Minor.ShouldBe(0); } [Fact] public void Should_default_to_version_one_point_one() { // Arrange, Act var result = _creator.Create(string.Empty); // Assert result.Major.ShouldBe(1); result.Minor.ShouldBe(1); } } ================================================ FILE: test/Ocelot.UnitTests/Consul/AgentServiceExtensions.cs ================================================ using Consul; namespace Ocelot.UnitTests.Consul; internal static class AgentServiceExtensions { public static AgentService WithServiceName(this AgentService agent, string serviceName) { agent.Service = serviceName; return agent; } public static AgentService WithPort(this AgentService agent, int port) { agent.Port = port; return agent; } public static AgentService WithAddress(this AgentService agent, string address) { agent.Address = address; return agent; } public static ServiceEntry ToServiceEntry(this AgentService agent) => new() { Service = agent, }; } ================================================ FILE: test/Ocelot.UnitTests/Consul/ConsulFileConfigurationRepositoryTests.cs ================================================ using Consul; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Ocelot.Cache; using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using Ocelot.Responses; using System.Text; namespace Ocelot.UnitTests.Consul; public class ConsulFileConfigurationRepositoryTests : UnitTest { private ConsulFileConfigurationRepository _repo; private readonly Mock> _options; private readonly Mock> _cache; private readonly Mock _factory; private readonly Mock _loggerFactory; private readonly Mock _client; private readonly Mock _kvEndpoint; private FileConfiguration _fileConfiguration; private Response _getResult; public ConsulFileConfigurationRepositoryTests() { _cache = new Mock>(); _loggerFactory = new Mock(); _options = new Mock>(); _factory = new Mock(); _client = new Mock(); _kvEndpoint = new Mock(); _client .Setup(x => x.KV) .Returns(_kvEndpoint.Object); _factory .Setup(x => x.Get(It.IsAny())) .Returns(_client.Object); _options .SetupGet(x => x.Value) .Returns(() => _fileConfiguration); } [Fact] public async Task Should_set_config() { // Arrange var config = GivenFakeFileConfiguration(); GivenWritingToConsulSucceeds(); // Act _ = await _repo.Set(config); // Assert ThenTheConfigurationIsStoredAs(config); } [Fact] public async Task Should_get_config() { // Arrange var config = _fileConfiguration = GivenFakeFileConfiguration(); GivenFetchFromConsulSucceeds(); // Act _getResult = await _repo.Get(); // Assert ThenTheConfigurationIs(config); } [Fact] public async Task Should_get_null_config() { // Arrange _fileConfiguration = GivenFakeFileConfiguration(); GivenFetchFromConsulReturnsNull(); // Act _getResult = await _repo.Get(); // Assert _getResult.Data.ShouldBeNull(); } [Fact] public async Task Should_get_config_from_cache() { // Arrange var config = _fileConfiguration = GivenFakeFileConfiguration(); GivenFetchFromCacheSucceeds(); // Act _getResult = await _repo.Get(); // Assert ThenTheConfigurationIs(config); } [Fact] public async Task Should_set_config_key() { // Arrange _fileConfiguration = GivenFakeFileConfiguration(); GivenTheConfigKeyComesFromFileConfig("Tom"); GivenFetchFromConsulSucceeds(); // Act _getResult = await _repo.Get(); // Assert ThenTheConfigKeyIs("Tom"); } [Fact] public async Task Should_set_default_config_key() { // Arrange _fileConfiguration = GivenFakeFileConfiguration(); GivenFetchFromConsulSucceeds(); // Act _getResult = await _repo.Get(); // Assert ThenTheConfigKeyIs("InternalConfiguration"); } private void ThenTheConfigKeyIs(string expected) { _kvEndpoint.Verify(x => x.Get(expected, It.IsAny()), Times.Once); } private void GivenTheConfigKeyComesFromFileConfig(string key) { _fileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.ConfigurationKey = key; _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); } private void ThenTheConfigurationIs(FileConfiguration config) { var expected = JsonConvert.SerializeObject(config, Formatting.Indented); var result = JsonConvert.SerializeObject(_getResult.Data, Formatting.Indented); result.ShouldBe(expected); } private void GivenWritingToConsulSucceeds() { var response = new WriteResult { Response = true, }; _kvEndpoint.Setup(x => x.Put(It.IsAny(), It.IsAny())).ReturnsAsync(response); } private void GivenFetchFromCacheSucceeds() { _cache.Setup(x => x.Get(It.IsAny(), It.IsAny())).Returns(_fileConfiguration); } private void GivenFetchFromConsulReturnsNull() { var result = new QueryResult(); _kvEndpoint.Setup(x => x.Get(It.IsAny(), It.IsAny())) .ReturnsAsync(result); } private void GivenFetchFromConsulSucceeds() { var json = JsonConvert.SerializeObject(_fileConfiguration, Formatting.Indented); var bytes = Encoding.UTF8.GetBytes(json); var kvp = new KVPair("OcelotConfiguration") { Value = bytes, }; var query = new QueryResult { Response = kvp, }; _kvEndpoint.Setup(x => x.Get(It.IsAny(), It.IsAny())) .ReturnsAsync(query); } private void ThenTheConfigurationIsStoredAs(FileConfiguration config) { var json = JsonConvert.SerializeObject(config, Formatting.Indented); var bytes = Encoding.UTF8.GetBytes(json); _kvEndpoint.Verify(x => x.Put(It.Is(k => k.Value.SequenceEqual(bytes)), It.IsAny()), Times.Once); } private FileConfiguration GivenFakeFileConfiguration() { var routes = new List { new() { DownstreamHostAndPorts = new List { new("123.12.12.12", 80), }, DownstreamScheme = Uri.UriSchemeHttps, DownstreamPathTemplate = "/asdfs/test/{test}", }, }; var globalConfiguration = new FileGlobalConfiguration { ServiceDiscoveryProvider = new FileServiceDiscoveryProvider { Scheme = Uri.UriSchemeHttps, Port = 198, Host = "blah", }, }; _fileConfiguration = new FileConfiguration { GlobalConfiguration = globalConfiguration, Routes = routes, }; _repo = new ConsulFileConfigurationRepository(_options.Object, _cache.Object, _factory.Object, _loggerFactory.Object); return _fileConfiguration; } } ================================================ FILE: test/Ocelot.UnitTests/Consul/ConsulProviderFactoryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.Consul; public sealed class ConsulProviderFactoryTests : UnitTest, IDisposable { private readonly ServiceProvider _provider; private readonly IServiceScope _scope; private readonly DefaultHttpContext _context = new(); public ConsulProviderFactoryTests() { var contextAccessor = new Mock(); _context.Items.Add(nameof(ConsulRegistryConfiguration), new ConsulRegistryConfiguration(null, null, 0, null, null)); contextAccessor.SetupGet(x => x.HttpContext).Returns(_context); var loggerFactory = new Mock(); var logger = new Mock(); loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); var consulFactory = new Mock(); var consulServiceBuilder = new Mock(); var services = new ServiceCollection(); services.AddSingleton(contextAccessor.Object); services.AddSingleton(consulFactory.Object); services.AddSingleton(loggerFactory.Object); services.AddScoped(_ => consulServiceBuilder.Object); _provider = services.BuildServiceProvider(true); // validate scopes!!! _scope = _provider.CreateScope(); _context.RequestServices = _scope.ServiceProvider; } public void Dispose() { _scope.Dispose(); _provider.Dispose(); } [Fact] public void Get_EmptyTypeName_ReturnedConsul() { // Arrange var emptyType = string.Empty; var route = GivenRoute(string.Empty); // Act var actual = CreateProvider(route, emptyType); // Assert actual.ShouldNotBeNull().ShouldBeOfType(); } [Fact] public void Get_PollConsulTypeName_ReturnedPollConsul() { // Arrange, Act var route = GivenRoute(string.Empty); var actual = CreateProvider(route, nameof(PollConsul)); // Assert actual.ShouldNotBeNull().ShouldBeOfType(); } [Fact] public void Get_RoutesWithTheSameServiceName_ReturnedSameProvider() { // Arrange, Act: 1 var route1 = GivenRoute("test"); var actual1 = CreateProvider(route1); // Arrange, Act: 2 var route2 = GivenRoute("test"); var actual2 = CreateProvider(route2); // Assert actual1.ShouldNotBeNull().ShouldBeOfType(); actual2.ShouldNotBeNull().ShouldBeOfType(); actual1.ShouldBeEquivalentTo(actual2); var provider1 = actual1 as PollConsul; var provider2 = actual2 as PollConsul; provider1.ServiceName.ShouldBeEquivalentTo(provider2.ServiceName); } [Fact] public void Get_MultipleServiceNames_ShouldReturnProviderAccordingToServiceName() { string[] serviceNames = new[] { "service1", "service2", "service3", "service4" }; var providersList = serviceNames.Select(DummyPollingConsulServiceFactory).ToList(); foreach (var serviceName in serviceNames) { var currentProvider = DummyPollingConsulServiceFactory(serviceName); providersList.ShouldContain(currentProvider); } var convertedProvidersList = providersList.Select(x => x as PollConsul).ToList(); convertedProvidersList.ForEach(x => x.ShouldNotBeNull()); foreach (var serviceName in serviceNames) { var cProvider = DummyPollingConsulServiceFactory(serviceName); var convertedCProvider = cProvider as PollConsul; convertedCProvider.ShouldNotBeNull(); var matchingProviders = convertedProvidersList .Where(x => x.ServiceName == convertedCProvider.ServiceName) .ToList(); matchingProviders.ShouldHaveSingleItem(); matchingProviders.First() .ShouldNotBeNull() .ServiceName.ShouldBeEquivalentTo(convertedCProvider.ServiceName); } } [Fact] [Trait("Bug", "2178")] public void Get_RootProvider_ShouldThrowInvalidOperationException() { // Arrange var route = GivenRoute(string.Empty); _context.RequestServices = _provider; // given service provider is root provider // Act Func consulProviderFactoryCall = () => CreateProvider(route); // Assert consulProviderFactoryCall.ShouldThrow(); } private IServiceDiscoveryProvider DummyPollingConsulServiceFactory(string serviceName) => CreateProvider(GivenRoute(serviceName)); private static DownstreamRoute GivenRoute(string serviceName) => new DownstreamRouteBuilder() .WithServiceName(serviceName) .Build(); private IServiceDiscoveryProvider CreateProvider(DownstreamRoute route, string providerType = ConsulProviderFactory.PollConsul) { var stopsFromPolling = 10000; return ConsulProviderFactory.Get.Invoke( _provider, new ServiceProviderConfiguration() { Type = providerType, Scheme = Uri.UriSchemeHttp, PollingInterval = stopsFromPolling, }, route); } } ================================================ FILE: test/Ocelot.UnitTests/Consul/DefaultConsulServiceBuilderTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Http; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using System.Reflection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Consul; public sealed class DefaultConsulServiceBuilderTests { private DefaultConsulServiceBuilder sut; private readonly Mock contextAccessor; private readonly Mock clientFactory; private readonly Mock loggerFactory; private readonly Mock logger; private ConsulRegistryConfiguration _configuration; public DefaultConsulServiceBuilderTests() { contextAccessor = new(); clientFactory = new(); clientFactory.Setup(x => x.Get(It.IsAny())) .Returns(new ConsulClient()); logger = new(); loggerFactory = new(); loggerFactory.Setup(x => x.CreateLogger()) .Returns(logger.Object); } private void Arrange([CallerMemberName] string testName = null) { _configuration = new(null, null, 0, testName, null); var context = new DefaultHttpContext(); context.Items.Add(nameof(ConsulRegistryConfiguration), _configuration); contextAccessor.SetupGet(x => x.HttpContext).Returns(context); sut = new DefaultConsulServiceBuilder(contextAccessor.Object, clientFactory.Object, loggerFactory.Object); } [Fact] public void Ctor_PrivateMembers_PropertiesAreInitialized() { Arrange(); var propClient = sut.GetType().GetProperty("Client", BindingFlags.NonPublic | BindingFlags.Instance); var propLogger = sut.GetType().GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance); var propConfiguration = sut.GetType().GetProperty("Configuration", BindingFlags.NonPublic | BindingFlags.Instance); // Act var actualConfiguration = propConfiguration.GetValue(sut); var actualClient = propClient.GetValue(sut); var actualLogger = propLogger.GetValue(sut); // Assert actualConfiguration.ShouldNotBeNull().ShouldBe(_configuration); actualClient.ShouldNotBeNull(); actualLogger.ShouldNotBeNull(); } private static Type Me { get; } = typeof(DefaultConsulServiceBuilder); private static MethodInfo GetNode { get; } = Me.GetMethod("GetNode", BindingFlags.NonPublic | BindingFlags.Instance); [Fact] public void GetNode_EntryBranch_ReturnsEntryNode() { Arrange(); Node node = new() { Name = nameof(GetNode_EntryBranch_ReturnsEntryNode) }; ServiceEntry entry = new() { Node = node }; // Act var actual = GetNode.Invoke(sut, new object[] { entry, null }) as Node; // Assert actual.ShouldNotBeNull().ShouldBe(node); actual.Name.ShouldBe(node.Name); } [Fact] public void GetNode_NodesBranch_ReturnsNodeFromCollection() { Arrange(); ServiceEntry entry = new() { Node = null, Service = new() { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }, }; Node[] nodes = null; // Act, Assert: nodes is null var actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; actual.ShouldBeNull(); // Arrange, Act, Assert: nodes has items, happy path var node = new Node { Address = nameof(GetNode_NodesBranch_ReturnsNodeFromCollection) }; nodes = new[] { node }; actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; actual.ShouldNotBeNull().ShouldBe(node); actual.Address.ShouldBe(entry.Service.Address); // Arrange, Act, Assert: nodes has items, some nulls in entry entry.Service.Address = null; actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; actual.ShouldBeNull(); entry.Service = null; actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; actual.ShouldBeNull(); entry = null; actual = GetNode.Invoke(sut, new object[] { entry, nodes }) as Node; actual.ShouldBeNull(); } private static MethodInfo GetDownstreamHost { get; } = Me.GetMethod("GetDownstreamHost", BindingFlags.NonPublic | BindingFlags.Instance); [Fact] public void GetDownstreamHost_BothBranches_NameOrAddress() { Arrange(); // Arrange, Act, Assert: node branch ServiceEntry entry = new() { Service = new() { Address = nameof(GetDownstreamHost_BothBranches_NameOrAddress) }, }; var node = new Node { Name = "test1" }; var actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; actual.ShouldNotBeNull().ShouldBe("test1"); // Arrange, Act, Assert: entry branch node = null; actual = GetDownstreamHost.Invoke(sut, new object[] { entry, node }) as string; actual.ShouldNotBeNull().ShouldBe(nameof(GetDownstreamHost_BothBranches_NameOrAddress)); } private static MethodInfo GetServiceVersion { get; } = Me.GetMethod("GetServiceVersion", BindingFlags.NonPublic | BindingFlags.Instance); [Fact] public void GetServiceVersion_TagsIsNull_EmptyString() { Arrange(); // Arrange, Act, Assert: collection is null ServiceEntry entry = new() { Service = new() { Tags = null }, }; Node node = null; var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; actual.ShouldBe(string.Empty); // Arrange, Act, Assert: collection has no version tag entry.Service.Tags = new[] { "test" }; actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; actual.ShouldBe(string.Empty); } [Fact] public void GetServiceVersion_HasTags_HappyPath() { Arrange(); // Arrange var tags = new string[] { "test", "version-v2" }; ServiceEntry entry = new() { Service = new() { Tags = tags }, }; Node node = null; // Act var actual = GetServiceVersion.Invoke(sut, new object[] { entry, node }) as string; // Assert actual.ShouldBe("v2"); } private static MethodInfo GetServiceTags { get; } = Me.GetMethod("GetServiceTags", BindingFlags.NonPublic | BindingFlags.Instance); [Fact] public void GetServiceTags_BothBranches() { Arrange(); // Arrange, Act, Assert: collection is null ServiceEntry entry = new() { Service = new() { Tags = null }, }; Node node = null; var actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; actual.ShouldNotBeNull().ShouldBeEmpty(); // Arrange, Act, Assert: happy path entry.Service.Tags = new string[] { "1", "2", "3" }; actual = GetServiceTags.Invoke(sut, new object[] { entry, node }) as IEnumerable; actual.ShouldNotBeNull().ShouldNotBeEmpty(); actual.Count().ShouldBe(3); actual.ShouldContain("3"); } } ================================================ FILE: test/Ocelot.UnitTests/Consul/OcelotBuilderExtensionsTests.cs ================================================ using Consul; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Provider.Consul; using Ocelot.Provider.Consul.Interfaces; using Ocelot.Values; using System.Reflection; namespace Ocelot.UnitTests.Consul; public class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment.Setup(e => e.ApplicationName) .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); return environment.Object; } [Fact] public void AddConsul_ShouldSetUpConsul() { // Arrange, Act var builder = _services.AddOcelot(_configRoot) .AddConsul(); // Assert builder.ShouldNotBeNull(); } [Fact] public void AddConfigStoredInConsul_ShouldSetUpConsul() { // Arrange, Act var builder = _services.AddOcelot(_configRoot) .AddConsul() .AddConfigStoredInConsul(); // Assert builder.ShouldNotBeNull(); } [Fact] public void AddConsulGeneric_TServiceBuilder_ShouldSetUpConsul() { // Arrange, Act var builder = _services .AddOcelot(_configRoot) .AddConsul(); // Assert builder.ShouldNotBeNull(); builder.Services.SingleOrDefault(s => s.ServiceType == typeof(IConsulServiceBuilder)).ShouldNotBeNull(); } } internal class FakeConsulServiceBuilder : IConsulServiceBuilder { public ConsulRegistryConfiguration Configuration => throw new NotImplementedException(); public IEnumerable BuildServices(ServiceEntry[] entries, Node[] nodes) => throw new NotImplementedException(); public Service CreateService(ServiceEntry serviceEntry, Node serviceNode) => throw new NotImplementedException(); public bool IsValid(ServiceEntry entry) => throw new NotImplementedException(); } ================================================ FILE: test/Ocelot.UnitTests/Consul/PollingConsulServiceDiscoveryProviderTests.cs ================================================ using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Provider.Consul; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.UnitTests.Consul; public class PollingConsulServiceDiscoveryProviderTests : UnitTest { private readonly int _delay; private readonly List _services; private readonly Mock _factory; private readonly Mock _logger; private readonly Mock _consulServiceDiscoveryProvider; private List _result; public PollingConsulServiceDiscoveryProviderTests() { _services = new List(); _delay = 1; _factory = new Mock(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _consulServiceDiscoveryProvider = new Mock(); } [Fact] public void Should_return_service_from_consul() { // Arrange var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); GivenConsulReturns(service); // Act WhenIGetTheServices(1); // Assert _result.Count.ShouldBe(1); } [Fact] public async Task Should_return_service_from_consul_without_delay() { // Arrange var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); GivenConsulReturns(service); // Act await WhenIGetTheServicesWithoutDelay(1); // Assert _result.Count.ShouldBe(1); } private void GivenConsulReturns(Service service) { _services.Add(service); _consulServiceDiscoveryProvider.Setup(x => x.GetAsync()).ReturnsAsync(_services); } private void WhenIGetTheServices(int expected) { var provider = new PollConsul(_delay, "test", _factory.Object, _consulServiceDiscoveryProvider.Object); var result = Wait.For(3_000).Until(() => { try { _result = provider.GetAsync().GetAwaiter().GetResult(); return _result.Count == expected; } catch (Exception) { return false; } }); result.ShouldBeTrue(); } private async Task WhenIGetTheServicesWithoutDelay(int expected) { var provider = new PollConsul(_delay, "test2", _factory.Object, _consulServiceDiscoveryProvider.Object); bool result; try { _result = await provider.GetAsync(); result = _result.Count == expected; } catch (Exception) { result = false; } result.ShouldBeTrue(); } } ================================================ FILE: test/Ocelot.UnitTests/Controllers/FileConfigurationControllerTests.cs ================================================ using Microsoft.AspNetCore.Mvc; using Ocelot.Administration; using Ocelot.Configuration.File; using Ocelot.Configuration.Repository; using Ocelot.Configuration.Setter; using Ocelot.Errors; using Ocelot.Responses; namespace Ocelot.UnitTests.Controllers; public class FileConfigurationControllerTests : UnitTest { private readonly FileConfigurationController _controller; private readonly Mock _repo; private readonly Mock _setter; public FileConfigurationControllerTests() { _repo = new Mock(); _setter = new Mock(); _controller = new FileConfigurationController(_repo.Object, _setter.Object); } [Fact] public async Task Should_get_file_configuration() { // Arrange var expected = new OkResponse(new FileConfiguration()); _repo.Setup(x => x.Get()).ReturnsAsync(expected); // Act var result = await _controller.Get(); // Assert _repo.Verify(x => x.Get(), Times.Once); } [Fact] public async Task Should_return_error_when_cannot_get_config() { // Arrange var expected = new ErrorResponse(It.IsAny()); _repo.Setup(x => x.Get()).ReturnsAsync(expected); // Act var result = await _controller.Get(); // Assert _repo.Verify(x => x.Get(), Times.Once); result.ShouldBeOfType(); } [Fact] public async Task Should_post_file_configuration() { // Arrange var expected = new FileConfiguration(); _setter.Setup(x => x.Set(It.IsAny())).ReturnsAsync(new OkResponse()); // Act var result = await _controller.Post(expected); // Assert _setter.Verify(x => x.Set(expected), Times.Once); } [Fact] public async Task Should_return_error_when_cannot_set_config() { // Arrange var expected = new FileConfiguration(); _setter.Setup(x => x.Set(It.IsAny())).ReturnsAsync(new ErrorResponse(new FakeError())); // Act var result = await _controller.Post(expected); // Assert _setter.Verify(x => x.Set(expected), Times.Once); result.ShouldBeOfType(); } [Fact] public async Task Should_catch_exception_when_cannot_set_config() { // Arrange var expected = new FileConfiguration(); _setter.Setup(x => x.Set(It.IsAny())) .Throws(new Exception("Service failed")); // Act var result = await _controller.Post(expected); // Assert _setter.Verify(x => x.Set(expected), Times.Once); result.ShouldBeOfType(); var actual = result as BadRequestObjectResult; Assert.StartsWith("Service failed:", actual.Value.ToString()); } private class FakeError : Error { public FakeError() : base(string.Empty, OcelotErrorCode.CannotAddDataError, 404) { } } } ================================================ FILE: test/Ocelot.UnitTests/Controllers/OutputCacheControllerTests.cs ================================================ using Microsoft.AspNetCore.Mvc; using Ocelot.Administration; using Ocelot.Cache; namespace Ocelot.UnitTests.Controllers; public class OutputCacheControllerTests : UnitTest { private readonly OutputCacheController _controller; private readonly Mock> _cache; public OutputCacheControllerTests() { _cache = new(); _controller = new(_cache.Object); } [Fact] public void Delete_ByKey_ClearedRegion() { // Arrange, Act var result = _controller.Delete("a"); // Assert result.ShouldBeOfType(); _cache.Verify(x => x.ClearRegion("a"), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/DependencyInjection/ConfigurationBuilderExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Newtonsoft.Json; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.DependencyInjection; public sealed class ConfigurationBuilderExtensionsTests : FileUnitTest { private IConfigurationRoot _configuration; private IConfigurationRoot _configRoot; private FileConfiguration _globalConfig; private FileConfiguration _routeA; private FileConfiguration _routeB; private FileConfiguration _aggregate; private FileConfiguration _envSpecific; private FileConfiguration _combinedFileConfiguration; private readonly Mock _hostingEnvironment; public ConfigurationBuilderExtensionsTests() { _hostingEnvironment = new Mock(); } protected override string EnvironmentName() => _hostingEnvironment?.Object?.EnvironmentName ?? base.EnvironmentName(); [Fact] public void Should_add_base_url_to_config() { // Arrange _configuration = new ConfigurationBuilder() .AddOcelotBaseUrl("test") .Build(); // Act var actual = _configuration.GetValue("BaseUrl", string.Empty); // Assert actual.ShouldBe("test"); } [Fact] [Trait("PR", "1227")] [Trait("Issue", "1216")] public void Should_merge_files_to_file() { // Arrange GivenTheEnvironmentIs(TestID); GivenMultipleConfigurationFiles(TestID); // Act WhenIAddOcelotConfiguration(TestID); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); TheOcelotPrimaryConfigFileExists(true); } [Fact] public void Should_store_given_configurations_when_provided_file_configuration_object() { // Arrange GivenTheEnvironmentIs(TestID); GivenCombinedFileConfigurationObject(); // Act WhenIAddOcelotConfigurationWithCombinedFileConfiguration(); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(true); } [Fact] public void Should_merge_files_except_env() { // Arrange GivenTheEnvironmentIs(TestID); GivenMultipleConfigurationFiles(TestID, true); // Act WhenIAddOcelotConfiguration(TestID); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); NotContainsEnvSpecificConfig(); } [Fact] public void Should_merge_files_in_specific_folder() { // Arrange GivenMultipleConfigurationFiles(TestID); // Act WhenIAddOcelotConfiguration(TestID); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); } [Fact] [Trait("PR", "1227")] [Trait("Issue", "1216")] public void Should_merge_files_to_memory() { // Arrange GivenTheEnvironmentIs(TestID); GivenMultipleConfigurationFiles(TestID); // Act WhenIAddOcelotConfiguration(TestID, MergeOcelotJson.ToMemory); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); TheOcelotPrimaryConfigFileExists(false); } [Fact] [Trait("PR", "1986")] [Trait("Issue", "1518")] public void Should_merge_files_with_null_environment() { // Arrange environmentConfigFileName = null; // Ups! const IWebHostEnvironment NullEnvironment = null; // Wow! GivenMultipleConfigurationFiles(TestID, false); // Act _configRoot = new ConfigurationBuilder() .AddOcelot(TestID, NullEnvironment, MergeOcelotJson.ToMemory, primaryConfigFileName, globalConfigFileName, environmentConfigFileName, false, false) .Build(); // Assert ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); TheOcelotPrimaryConfigFileExists(false); } [Fact] [Trait("Bug", "2084")] public void Should_use_relative_path_for_global_config() { // Arrange GivenMultipleConfigurationFiles(TestID); // Act WhenIAddOcelotConfigurationWithDefaultFilePaths(TestID); // Assert var config = ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(false); config.ShouldNotBeNull().GlobalConfiguration.RequestIdKey.ShouldBe(nameof(Should_use_relative_path_for_global_config)); } private void GivenCombinedFileConfigurationObject([CallerMemberName] string testName = null) { _combinedFileConfiguration = new FileConfiguration { GlobalConfiguration = GetFileGlobalConfigurationData(testName), Routes = GetServiceARoutes().Concat(GetServiceBRoutes()).Concat(GetEnvironmentSpecificRoutes()).ToList(), Aggregates = GetFileAggregatesRouteData(), }; } private void GivenMultipleConfigurationFiles(string folder, bool withEnvironment = false, [CallerMemberName] string testName = null) { _globalConfig = new() { GlobalConfiguration = GetFileGlobalConfigurationData(testName) }; _routeA = new() { Routes = GetServiceARoutes() }; _routeB = new() { Routes = GetServiceBRoutes() }; _aggregate = new() { Aggregates = GetFileAggregatesRouteData() }; _envSpecific = new() { Routes = GetEnvironmentSpecificRoutes() }; var configParts = new Dictionary { { "global", _globalConfig }, { "routesA", _routeA }, { "routesB", _routeB }, { "aggregates", _aggregate }, }; if (withEnvironment) { configParts.Add(EnvironmentName(), _envSpecific); } foreach (var part in configParts) { var filename = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, part.Key)); File.WriteAllText(filename, JsonConvert.SerializeObject(part.Value, Formatting.Indented)); files.Add(filename); } } private static FileGlobalConfiguration GetFileGlobalConfigurationData(string requestIdKey = null) => new() { BaseUrl = "BaseUrl", RateLimitOptions = new() { HttpStatusCode = 500, ClientIdHeader = "ClientIdHeader", QuotaExceededMessage = "QuotaExceededMessage", RateLimitCounterPrefix = "RateLimitCounterPrefix", }, ServiceDiscoveryProvider = new() { Scheme = "https", Host = "Host", Port = 80, Type = "Type", }, RequestIdKey = requestIdKey ?? "RequestIdKey", }; private static List GetFileAggregatesRouteData() => new() { new() { RouteKeys = ["KeyB", "KeyBB"], UpstreamPathTemplate = "UpstreamPathTemplate", }, }; private static FileRoute GetRoute(string suffix) => new() { DownstreamScheme = "DownstreamScheme" + suffix, DownstreamPathTemplate = "DownstreamPathTemplate" + suffix, Key = "Key" + suffix, UpstreamHost = "UpstreamHost" + suffix, UpstreamHttpMethod = [ "UpstreamHttpMethod" + suffix ], DownstreamHostAndPorts = new() { new("Host"+suffix, 80), }, }; private static List GetServiceARoutes() => new() { GetRoute("A") }; private static List GetServiceBRoutes() => new() { GetRoute("B"), GetRoute("BB") }; private static List GetEnvironmentSpecificRoutes() => new() { GetRoute("Spec") }; private void GivenTheEnvironmentIs(string folder, [CallerMemberName] string testName = null) { _hostingEnvironment.SetupGet(x => x.EnvironmentName).Returns(testName); environmentConfigFileName = Path.Combine(folder, string.Format(ConfigurationBuilderExtensions.EnvironmentConfigFile, testName)); files.Add(environmentConfigFileName); } private void WhenIAddOcelotConfigurationWithCombinedFileConfiguration() { _configRoot = new ConfigurationBuilder() .AddOcelot(_combinedFileConfiguration, primaryConfigFileName, false, false) .Build(); } private void WhenIAddOcelotConfiguration(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile) { _configRoot = new ConfigurationBuilder() .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, primaryConfigFileName, globalConfigFileName, environmentConfigFileName, false, false) .Build(); } private void WhenIAddOcelotConfigurationWithDefaultFilePaths(string folder, MergeOcelotJson mergeOcelotJson = MergeOcelotJson.ToFile) { _configRoot = new ConfigurationBuilder() .AddOcelot(folder, _hostingEnvironment.Object, mergeOcelotJson, optional: false, reloadOnChange: false) .Build(); } private FileConfiguration ThenTheConfigsAreMergedAndAddedInApplicationConfiguration(bool useCombinedConfig) { var fc = _configRoot.Get(); fc.GlobalConfiguration.BaseUrl.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.BaseUrl : _globalConfig.GlobalConfiguration.BaseUrl); fc.GlobalConfiguration.RateLimitOptions.ClientIdHeader.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.ClientIdHeader : _globalConfig.GlobalConfiguration.RateLimitOptions.ClientIdHeader); fc.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.DisableRateLimitHeaders); fc.GlobalConfiguration.RateLimitOptions.EnableHeaders.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.EnableHeaders : _globalConfig.GlobalConfiguration.RateLimitOptions.EnableHeaders); fc.GlobalConfiguration.RateLimitOptions.HttpStatusCode.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.HttpStatusCode : _globalConfig.GlobalConfiguration.RateLimitOptions.HttpStatusCode); fc.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage : _globalConfig.GlobalConfiguration.RateLimitOptions.QuotaExceededMessage); fc.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix : _globalConfig.GlobalConfiguration.RateLimitOptions.RateLimitCounterPrefix); fc.GlobalConfiguration.RequestIdKey.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.RequestIdKey : _globalConfig.GlobalConfiguration.RequestIdKey); fc.GlobalConfiguration.ServiceDiscoveryProvider.Scheme.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Scheme : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Scheme); fc.GlobalConfiguration.ServiceDiscoveryProvider.Host.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Host : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Host); fc.GlobalConfiguration.ServiceDiscoveryProvider.Port.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Port : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Port); fc.GlobalConfiguration.ServiceDiscoveryProvider.Type.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.GlobalConfiguration.ServiceDiscoveryProvider.Type : _globalConfig.GlobalConfiguration.ServiceDiscoveryProvider.Type); fc.Routes.Count.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.Routes.Count : _routeA.Routes.Count + _routeB.Routes.Count); fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].DownstreamPathTemplate : _routeA.Routes[0].DownstreamPathTemplate)); fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].DownstreamPathTemplate : _routeB.Routes[0].DownstreamPathTemplate)); fc.Routes.ShouldContain(x => x.DownstreamPathTemplate == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].DownstreamPathTemplate : _routeB.Routes[1].DownstreamPathTemplate)); fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].DownstreamScheme : _routeA.Routes[0].DownstreamScheme)); fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].DownstreamScheme : _routeB.Routes[0].DownstreamScheme)); fc.Routes.ShouldContain(x => x.DownstreamScheme == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].DownstreamScheme : _routeB.Routes[1].DownstreamScheme)); fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].Key : _routeA.Routes[0].Key)); fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].Key : _routeB.Routes[0].Key)); fc.Routes.ShouldContain(x => x.Key == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].Key : _routeB.Routes[1].Key)); fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[0].UpstreamHost : _routeA.Routes[0].UpstreamHost)); fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[1].UpstreamHost : _routeB.Routes[0].UpstreamHost)); fc.Routes.ShouldContain(x => x.UpstreamHost == (useCombinedConfig ? _combinedFileConfiguration.Routes[2].UpstreamHost : _routeB.Routes[1].UpstreamHost)); fc.Aggregates.Count.ShouldBe(useCombinedConfig ? _combinedFileConfiguration.Aggregates.Count :_aggregate.Aggregates.Count); return fc; } private void NotContainsEnvSpecificConfig() { var fc = _configRoot.Get(); fc.Routes.ShouldNotContain(x => x.DownstreamScheme == _envSpecific.Routes[0].DownstreamScheme); fc.Routes.ShouldNotContain(x => x.DownstreamPathTemplate == _envSpecific.Routes[0].DownstreamPathTemplate); fc.Routes.ShouldNotContain(x => x.Key == _envSpecific.Routes[0].Key); } private void TheOcelotPrimaryConfigFileExists(bool expected) => File.Exists(primaryConfigFileName).ShouldBe(expected); } ================================================ FILE: test/Ocelot.UnitTests/DependencyInjection/OcelotBuilderTests.cs ================================================ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization.Infrastructure; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.MiddlewareAnalysis; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Setter; using Ocelot.DependencyInjection; using Ocelot.Infrastructure; using Ocelot.LoadBalancer.Creators; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Multiplexer; using Ocelot.Requester; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.UnitTests.Requester; using Ocelot.Values; using System.Reflection; using System.Text.Encodings.Web; using static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests; namespace Ocelot.UnitTests.DependencyInjection; public class OcelotBuilderTests : UnitTest { private readonly IConfiguration _configRoot; private readonly IServiceCollection _services; private IServiceProvider _serviceProvider; private IOcelotBuilder _ocelotBuilder; public OcelotBuilderTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment.Setup(e => e.ApplicationName) .Returns(typeof(OcelotBuilderTests).GetTypeInfo().Assembly.GetName().Name); return environment.Object; } [Fact] [Trait("Feat", "224")] // https://github.com/ThreeMammals/Ocelot/pull/224 [Trait("Feat", "269")] // https://github.com/ThreeMammals/Ocelot/pull/269 [Trait("Bug", "456")] // https://github.com/ThreeMammals/Ocelot/pull/456 public void AddDelegatingHandler_Generic_NotGlobal() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddDelegatingHandler(); _ocelotBuilder.AddDelegatingHandler(); // Assert ThenTheProviderIsRegisteredAndReturnsSpecificHandlers(); ThenTheSpecificHandlersAreTransient(); } [Fact] [Trait("Feat", "224")] // https://github.com/ThreeMammals/Ocelot/pull/224 [Trait("Feat", "269")] // https://github.com/ThreeMammals/Ocelot/pull/269 [Trait("Bug", "456")] // https://github.com/ThreeMammals/Ocelot/pull/456 public void AddDelegatingHandler_Generic_Global() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddDelegatingHandler(true); _ocelotBuilder.AddDelegatingHandler(true); // Assert ThenTheProviderIsRegisteredAndReturnsHandlers(); ThenTheGlobalHandlersAreTransient(); } [Fact] [Trait("Feat", "943")] // https://github.com/ThreeMammals/Ocelot/pull/943 public void AddDelegatingHandler_Type_TypeCheck() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act var ex = Assert.Throws( () => _ocelotBuilder.AddDelegatingHandler(typeof(OcelotBuilderTests))); // OcelotBuilderTests type is not DelegatingHandler one // Assert Assert.Equal("delegateType", ex.ParamName); Assert.Equal(nameof(OcelotBuilderTests), (string)ex.ActualValue); Assert.Equal($"It is not a delegating handler (Parameter 'delegateType'){Environment.NewLine}Actual value was OcelotBuilderTests.", ex.Message); } [Fact] [Trait("Feat", "943")] // https://github.com/ThreeMammals/Ocelot/pull/943 public void AddDelegatingHandler_Type_Global() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandler), true); _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandlerTwo), true); // Assert ThenTheProviderIsRegisteredAndReturnsHandlers(); ThenTheGlobalHandlersAreTransient(); } [Fact] [Trait("Feat", "943")] // https://github.com/ThreeMammals/Ocelot/pull/943 public void AddDelegatingHandler_Type_NotGlobal() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandler)); _ocelotBuilder.AddDelegatingHandler(typeof(FakeDelegatingHandlerTwo)); // Assert ThenTheProviderIsRegisteredAndReturnsSpecificHandlers(); ThenTheSpecificHandlersAreTransient(); } [Fact] public void Should_set_up_services() { // Arrange, Act, Assert _ocelotBuilder = _services.AddOcelot(_configRoot); } [Fact] public void Should_return_ocelot_builder() { // Arrange, Act _ocelotBuilder = _services.AddOcelot(_configRoot); // Assert _ocelotBuilder.ShouldBeOfType(); } [Fact] public void Should_use_logger_factory() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); _serviceProvider = _services.BuildServiceProvider(true); // Act var logger = _serviceProvider.GetService(); // Assert logger.ShouldNotBeNull(); } [Fact] public void Should_set_up_without_passing_in_config() { // Arrange, Act, Assert _ocelotBuilder = _services.AddOcelot(); } [Fact] public void Should_add_singleton_defined_aggregators() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddSingletonDefinedAggregator(); _ocelotBuilder.AddSingletonDefinedAggregator(); // Assert ThenTheProviderIsRegisteredAndReturnsSpecificAggregators(); // Then The Aggregators Are Singleton var aggregators = _serviceProvider.GetServices().ToList(); var first = aggregators[0]; aggregators = _serviceProvider.GetServices().ToList(); var second = aggregators[0]; first.ShouldBe(second); } [Fact] public void Should_add_transient_defined_aggregators() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddTransientDefinedAggregator(); _ocelotBuilder.AddTransientDefinedAggregator(); // Assert ThenTheProviderIsRegisteredAndReturnsSpecificAggregators(); // Then The Aggregators Are Transient var aggregators = _serviceProvider.GetServices().ToList(); var first = aggregators[0]; aggregators = _serviceProvider.GetServices().ToList(); var second = aggregators[0]; first.ShouldNotBe(second); } [Fact] public void Should_add_custom_load_balancer_creators_by_default_ctor() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddCustomLoadBalancer(); // Assert ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators(); } [Fact] public void Should_add_custom_load_balancer_creators_by_factory_method() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddCustomLoadBalancer(() => new FakeCustomLoadBalancer()); // Assert ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators(); } [Fact] public void Should_add_custom_load_balancer_creators_by_di_factory_method() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddCustomLoadBalancer(provider => new FakeCustomLoadBalancer()); // Assert ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators(); } [Fact] public void Should_add_custom_load_balancer_creators_by_factory_method_with_arguments() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddCustomLoadBalancer((route, discoveryProvider) => new FakeCustomLoadBalancer()); // Assert ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators(); } [Fact] public void Should_replace_iplaceholder() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddConfigPlaceholders(); // Assert _serviceProvider = _services.BuildServiceProvider(true); var placeholders = _serviceProvider.GetService(); placeholders.ShouldBeOfType(); } [Fact] public void Should_add_custom_load_balancer_creators() { // Arrange _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddCustomLoadBalancer((provider, route, discoveryProvider) => new FakeCustomLoadBalancer()); // Assert ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators(); } [Fact] public void Should_use_default_mvc_builder() { // Arrange, Act _ocelotBuilder = _services.AddOcelot(); // Assert CstorShouldUseDefaultBuilderToInitMvcCoreBuilder(); } private void CstorShouldUseDefaultBuilderToInitMvcCoreBuilder() { _ocelotBuilder.ShouldNotBeNull(); _ocelotBuilder.MvcCoreBuilder.ShouldNotBeNull(); _serviceProvider = _services.BuildServiceProvider(true); using IServiceScope scope = _serviceProvider.CreateScope(); // .AddMvcCore() _serviceProvider.GetServices>() .FirstOrDefault(s => s.GetType().Name == "MvcCoreMvcOptionsSetup") .ShouldNotBeNull(); // .AddLogging() _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService>() .ShouldNotBeNull(); // .AddMiddlewareAnalysis() _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); // .AddWebEncoders() _serviceProvider.GetService().ShouldNotBeNull(); _serviceProvider.GetService().ShouldNotBeNull(); _serviceProvider.GetService().ShouldNotBeNull(); // .AddApplicationPart(assembly) IList list = _ocelotBuilder.MvcCoreBuilder.PartManager.ApplicationParts; list.ShouldNotBeNull().Count.ShouldBe(2); list.ShouldContain(part => part.Name == "Ocelot"); list.ShouldContain(part => part.Name == "Ocelot.UnitTests"); // .AddControllersAsServices() _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); // .AddAuthorization() -> .AddAuthorizationCore() //_serviceProvider.GetService() // .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService().ShouldNotBeNull() #if NET10_0_OR_GREATER .GetType().Name.ShouldBe("DefaultAuthorizationServiceImpl"); #else .ShouldBeOfType(); #endif _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); _serviceProvider.GetService() .ShouldNotBeNull().ShouldBeOfType(); scope.ServiceProvider.GetService().ShouldNotBeNull() #if NET10_0_OR_GREATER .GetType().Name.ShouldBe("AuthenticationServiceImpl"); #else .ShouldBeOfType(); #endif _serviceProvider.GetService() .ShouldNotBeNull() .GetType().Name.ShouldBe("AuthorizationApplicationModelProvider"); // .AddNewtonsoftJson() _serviceProvider.GetServices>() .FirstOrDefault(s => s.GetType().Name == "NewtonsoftJsonMvcOptionsSetup") .ShouldNotBeNull(); _serviceProvider.GetService>() .ShouldNotBeNull() .GetType().Name.ShouldBe("NewtonsoftJsonResultExecutor"); _serviceProvider.GetService() .ShouldNotBeNull() .GetType().Name.ShouldBe("NewtonsoftJsonHelper"); } [Fact] public void Should_use_custom_mvc_builder_no_configuration() { // Arrange, Act WhenISetupOcelotServicesWithCustomMvcBuider(); // Assert CstorShouldUseCustomBuilderToInitMvcCoreBuilder(); ShouldFindConfiguration(); } [Theory] [Trait("PR", "1986")] [Trait("Issue", "1518")] [InlineData(false)] [InlineData(true)] public void Should_use_custom_mvc_builder_with_configuration(bool hasConfig) { // Arrange, Act WhenISetupOcelotServicesWithCustomMvcBuider( hasConfig ? _configRoot : null, true); // Assert CstorShouldUseCustomBuilderToInitMvcCoreBuilder(); ShouldFindConfiguration(); } [Fact] public void CreateInstance_CreatedFromImplementationInstance() { // Arrange var method = typeof(OcelotBuilder).GetMethod("CreateInstance", BindingFlags.NonPublic | BindingFlags.Static); ServiceDescriptor descriptor = new(GetType(), this); // Act var result = method.Invoke(null, [null, descriptor]); // Assert Assert.NotNull(result); Assert.IsType(result); Assert.Equal(this, result); } [Fact] public void CreateInstance_CreatedByImplementationFactory() { // Arrange var method = typeof(OcelotBuilder).GetMethod("CreateInstance", BindingFlags.NonPublic | BindingFlags.Static); object factory(IServiceProvider p) => this; ServiceDescriptor descriptor = new(GetType(), factory, ServiceLifetime.Singleton); // Act var result = method.Invoke(null, [null, descriptor]); // Assert Assert.NotNull(result); Assert.IsType(result); Assert.Equal(this, result); } private bool _fakeCustomBuilderCalled; private IMvcCoreBuilder FakeCustomBuilder(IMvcCoreBuilder builder, Assembly assembly) { _fakeCustomBuilderCalled = true; return builder.AddJsonOptions(options => { options.JsonSerializerOptions.WriteIndented = true; }); } private void WhenISetupOcelotServicesWithCustomMvcBuider(IConfiguration configuration = null, bool useConfigParam = false) { _fakeCustomBuilderCalled = false; _ocelotBuilder = !useConfigParam ? _services.AddOcelotUsingBuilder(FakeCustomBuilder) : _services.AddOcelotUsingBuilder(configuration, FakeCustomBuilder); } private void CstorShouldUseCustomBuilderToInitMvcCoreBuilder() { _fakeCustomBuilderCalled.ShouldBeTrue(); _ocelotBuilder.ShouldNotBeNull(); _ocelotBuilder.MvcCoreBuilder.ShouldNotBeNull(); _serviceProvider = _services.BuildServiceProvider(true); // .AddMvcCore() _serviceProvider.GetServices>() .FirstOrDefault(s => s.GetType().Name == "MvcCoreMvcOptionsSetup") .ShouldNotBeNull(); // .AddJsonOptions(options => { }) _serviceProvider.GetService>() .ShouldNotBeNull().ShouldBeOfType>(); _serviceProvider.GetService>() .ShouldNotBeNull().ShouldBeOfType>(); } private void ShouldFindConfiguration() { _ocelotBuilder.ShouldNotBeNull(); var actual = _ocelotBuilder.Configuration.ShouldNotBeNull(); actual.Equals(_configRoot).ShouldBeTrue(); // check references equality actual.ShouldBe(_configRoot); } private void ThenTheSpecificHandlersAreTransient() { var handlers = _serviceProvider.GetServices().ToList(); var first = handlers[0]; handlers = _serviceProvider.GetServices().ToList(); var second = handlers[0]; first.ShouldNotBe(second); } private void ThenTheGlobalHandlersAreTransient() { var handlers = _serviceProvider.GetServices().ToList(); var first = handlers[0].DelegatingHandler; handlers = _serviceProvider.GetServices().ToList(); var second = handlers[0].DelegatingHandler; first.ShouldNotBe(second); } private void ThenTheProviderIsRegisteredAndReturnsHandlers() { _serviceProvider = _services.BuildServiceProvider(true); var handlers = _serviceProvider.GetServices().ToList(); handlers[0].DelegatingHandler.ShouldBeOfType(); handlers[1].DelegatingHandler.ShouldBeOfType(); } private void ThenTheProviderIsRegisteredAndReturnsSpecificHandlers() { _serviceProvider = _services.BuildServiceProvider(true); var handlers = _serviceProvider.GetServices().ToList(); handlers[0].ShouldBeOfType(); handlers[1].ShouldBeOfType(); } private void ThenTheProviderIsRegisteredAndReturnsSpecificAggregators() { _serviceProvider = _services.BuildServiceProvider(true); var handlers = _serviceProvider.GetServices().ToList(); handlers[0].ShouldBeOfType(); handlers[1].ShouldBeOfType(); } private void ThenTheProviderIsRegisteredAndReturnsBothBuiltInAndCustomLoadBalancerCreators() { _serviceProvider = _services.BuildServiceProvider(true); var creators = _serviceProvider.GetServices().ToList(); creators.Count(c => c.GetType() == typeof(NoLoadBalancerCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(RoundRobinCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(CookieStickySessionsCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(LeastConnectionCreator)).ShouldBe(1); creators.Count(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator)).ShouldBe(1); // Call Create var creator = creators.Single(c => c.GetType() == typeof(DelegateInvokingLoadBalancerCreator)); Assert.NotNull(creator); var route = new DownstreamRouteBuilder().Build(); var provider = _serviceProvider.GetService(); var response = creator.Create(route, provider); Assert.NotNull(response); Assert.False(response.IsError); Assert.NotNull(response.Data); Assert.IsType(response.Data); } private class FakeCustomLoadBalancer : ILoadBalancer { public string Type => nameof(FakeCustomLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/DependencyInjection/ServiceCollectionExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Ocelot.DependencyInjection; using System.Reflection; using Extensions = Ocelot.DependencyInjection.ServiceCollectionExtensions; namespace Ocelot.UnitTests.DependencyInjection; public class ServiceCollectionExtensionsTests { [Fact] [Trait("PR", "1986")] [Trait("Issue", "1518")] public void AddOcelot_NoConfiguration_DefaultConfiguration() { // Arrange var services = new ServiceCollection(); // Act var ocelot = services.AddOcelot(); // Assert ocelot.ShouldNotBeNull() .Configuration.ShouldNotBeNull(); } [Theory] [Trait("PR", "1986")] [Trait("Issue", "1518")] [InlineData(false)] [InlineData(true)] public void FindConfiguration_HasDescriptor_HappyPath(bool hasConfig) { // Arrange IConfiguration config = hasConfig ? new ConfigurationBuilder().Build() : null; var descriptor = new ServiceDescriptor(typeof(IConfiguration), (p) => config, ServiceLifetime.Transient); var services = new ServiceCollection().Add(descriptor); IWebHostEnvironment env = null; // Act var method = typeof(Extensions).GetMethod("FindConfiguration", BindingFlags.NonPublic | BindingFlags.Static); var actual = (IConfiguration)method.Invoke(null, new object[] { services, env }); // Assert actual.ShouldNotBeNull(); if (hasConfig) { actual.Equals(config).ShouldBeTrue(); } } [Fact] [Trait("PR", "1986")] [Trait("Issue", "1518")] public void AddOcelotUsingBuilder_NoConfigurationParam_ShouldFindConfiguration() { // Arrange var services = new ServiceCollection(); var config = new ConfigurationBuilder().Build(); services.AddSingleton(config); // Act var ocelot = services.AddOcelotUsingBuilder(null, CustomBuilder); // Assert AssertConfiguration(ocelot, config); } [Theory] [Trait("PR", "1986")] [Trait("Issue", "1518")] [InlineData(false)] [InlineData(true)] public void AddOcelotUsingBuilder_WithConfigurationParam_ShouldFindConfiguration(bool shouldFind) { // Arrange var services = new ServiceCollection(); var config = new ConfigurationBuilder().Build(); if (shouldFind) { services.AddSingleton(config); } // Act var ocelot = services.AddOcelotUsingBuilder(shouldFind ? null : config, CustomBuilder); // Assert AssertConfiguration(ocelot, config); } private void AssertConfiguration(IOcelotBuilder ocelot, IConfiguration config) { ocelot.ShouldNotBeNull(); var actual = ocelot.Configuration.ShouldNotBeNull(); actual.Equals(config).ShouldBeTrue(); // check references equality actual.ShouldBe(config); Assert.Equal(1, _count); } private int _count; private IMvcCoreBuilder CustomBuilder(IMvcCoreBuilder builder, Assembly assembly) { _count++; return builder; } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamPathManipulation/ChangeDownstreamPathTemplateTests.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.PathManipulation; using Ocelot.Responses; using Ocelot.UnitTests.Responder; using Ocelot.Values; using System.Security.Claims; namespace Ocelot.UnitTests.DownstreamPathManipulation; [Trait("Feat", "968")] // https://github.com/ThreeMammals/Ocelot/pull/968 [Trait("Release", "13.8.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0 public class ChangeDownstreamPathTemplateTests : UnitTest { private readonly ChangeDownstreamPathTemplate _changeDownstreamPath; private DownstreamPathTemplate _downstreamPathTemplate; private readonly Mock _parser; private List _configuration; private List _claims; private Response _result; private Response _claimValue; private List _placeholderValues; public ChangeDownstreamPathTemplateTests() { _parser = new Mock(); _changeDownstreamPath = new ChangeDownstreamPathTemplate(_parser.Object); } [Fact] public void Should_change_downstream_path_request() { // Arrange _claims = new List { new("test", "data"), }; _placeholderValues = new List(); _configuration = new List { new("path-key", string.Empty, string.Empty, 0), }; _downstreamPathTemplate = new DownstreamPathTemplate("/api/test/{path-key}"); GivenTheClaimParserReturns(new OkResponse("value")); // Act WhenIChangeDownstreamPath(); // Assert _result.IsError.ShouldBeFalse(); ThenClaimDataIsContainedInPlaceHolder("{path-key}", "value"); } [Fact] public void Should_replace_existing_placeholder_value() { // Arrange _claims = new List { new("test", "data"), }; _placeholderValues = new List { new("{path-key}", "old_value"), }; _configuration = new List { new("path-key", string.Empty, string.Empty, 0), }; _downstreamPathTemplate = new DownstreamPathTemplate("/api/test/{path-key}"); GivenTheClaimParserReturns(new OkResponse("value")); // Act WhenIChangeDownstreamPath(); // Assert _result.IsError.ShouldBeFalse(); ThenClaimDataIsContainedInPlaceHolder("{path-key}", "value"); } [Fact] public void Should_return_error_when_no_placeholder_in_downstream_path() { // Arrange _claims = new List { new("test", "data"), }; _placeholderValues = new List(); _configuration = new List { new("path-key", string.Empty, string.Empty, 0), }; _downstreamPathTemplate = new DownstreamPathTemplate("/api/test"); GivenTheClaimParserReturns(new OkResponse("value")); // Act WhenIChangeDownstreamPath(); // Assert _result.IsError.ShouldBe(true); _result.Errors.Count.ShouldBe(1); _result.Errors.First().ShouldBeOfType(); } [Fact] public void Should_return_error_when_claim_parser_returns_error() { // Arrange _claims = new List { new("test", "data"), }; _placeholderValues = new List(); _configuration = new List { new("path-key", string.Empty, string.Empty, 0), }; _downstreamPathTemplate = new DownstreamPathTemplate("/api/test/{path-key}"); GivenTheClaimParserReturns(new ErrorResponse(new List { new AnyError(), })); // Act WhenIChangeDownstreamPath(); // Assert _result.IsError.ShouldBe(true); } private void GivenTheClaimParserReturns(Response claimValue) { _claimValue = claimValue; _parser.Setup(x => x.GetValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_claimValue); } private void WhenIChangeDownstreamPath() => _result = _changeDownstreamPath.ChangeDownstreamPath(_configuration, _claims, _downstreamPathTemplate, _placeholderValues); private void ThenClaimDataIsContainedInPlaceHolder(string name, string value) { var placeHolder = _placeholderValues.FirstOrDefault(ph => ph.Name == name && ph.Value == value); placeHolder.ShouldNotBeNull(); _placeholderValues.Count.ShouldBe(1); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamPathManipulation/ClaimsToDownstreamPathMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamPathManipulation.Middleware; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.PathManipulation; using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Values; using System.Security.Claims; namespace Ocelot.UnitTests.DownstreamPathManipulation; [Trait("Feat", "968")] // https://github.com/ThreeMammals/Ocelot/pull/968 [Trait("Release", "13.8.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/13.8.0 public class ClaimsToDownstreamPathMiddlewareTests : UnitTest { private readonly Mock _changePath; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ClaimsToDownstreamPathMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ClaimsToDownstreamPathMiddlewareTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _changePath = new Mock(); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); _middleware = new ClaimsToDownstreamPathMiddleware(_next, _loggerFactory.Object, _changePath.Object); } [Fact] public async Task Should_call_add_queries_correctly() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToDownstreamPath(new List { new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); var downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); // Arrange: Given The Down Stream Route Is _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); // Arrange: Given The Change Downstream Path Returns Ok _changePath.Setup(x => x.ChangeDownstreamPath( It.IsAny>(), It.IsAny>(), It.IsAny(), It.IsAny>())) .Returns(new OkResponse()); // Act await _middleware.Invoke(_httpContext); // Assert _changePath.Verify(x => x.ChangeDownstreamPath( It.IsAny>(), It.IsAny>(), _httpContext.Items.DownstreamRoute().DownstreamPathTemplate, _httpContext.Items.TemplatePlaceholderNameAndValues()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Infrastructure.Extensions; using Ocelot.LoadBalancer.Balancers; using Ocelot.Responses; using Ocelot.Values; using System.Reflection; namespace Ocelot.UnitTests.DownstreamRouteFinder; public class DiscoveryDownstreamRouteFinderTests : UnitTest { private readonly DiscoveryDownstreamRouteFinder _finder; private QoSOptions _qoSOptions; private LoadBalancerOptions _loadBalancerOptions; private Response _result; private string _upstreamHost; private string _upstreamUrlPath; private string _upstreamHttpMethod; private IHeaderDictionary _upstreamHeaders; private IInternalConfiguration _configuration; private Response _resultTwo; private readonly string _upstreamQuery; private readonly Mock _upstreamHeaderTemplatePatternCreator = new(); private readonly HttpHandlerOptions _handlerOptions; private readonly MetadataOptions _metadataOptions; private readonly RateLimitOptions _rateLimitOptions; public DiscoveryDownstreamRouteFinderTests() { _qoSOptions = new(new FileQoSOptions()); _handlerOptions = new(); _loadBalancerOptions = new(nameof(NoLoadBalancer), default, default); _metadataOptions = new MetadataOptions(); _rateLimitOptions = new RateLimitOptions(); _finder = new(new RouteKeyCreator(), _upstreamHeaderTemplatePatternCreator.Object); _upstreamQuery = string.Empty; } [Fact] public void Should_create_downstream_route() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); // Act WhenICreate(); // Assert ThenTheDownstreamRouteIsCreated(); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "1229")] public void Should_create_downstream_route_with_rate_limit_options() { // Arrange var rateLimitOptions = new RateLimitOptions() { EnableRateLimiting = true, ClientIdHeader = "test", }; var downstreamRoute = new DownstreamRouteBuilder() .WithServiceName("auth") .WithRateLimitOptions(rateLimitOptions) .WithLoadBalancerKey("|auth") .WithLoadBalancerOptions(_loadBalancerOptions) .WithQosOptions(_qoSOptions) .WithHttpHandlerOptions(_handlerOptions) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); var route = new Route(true, downstreamRoute); // create dynamic route GivenInternalConfiguration(route); GivenTheConfiguration(); // Act WhenICreate(); // Assert ThenTheDownstreamRouteIsCreated(lbKey: downstreamRoute.LoadBalancerKey); // Assert: With RateLimitOptions var actual = _result.Data.Route.DownstreamRoute[0].RateLimitOptions; actual.EnableRateLimiting.ShouldBeTrue(); actual.EnableRateLimiting.ShouldBe(rateLimitOptions.EnableRateLimiting); actual.ClientIdHeader.ShouldBe(rateLimitOptions.ClientIdHeader); } [Fact] public void Should_cache_downstream_route() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); _upstreamUrlPath = "/geoffisthebest/"; // Act WhenICreate(); GivenTheConfiguration(); _upstreamUrlPath = "/geoffisthebest/"; WhenICreateAgain(); // Assert _result.ShouldBe(_resultTwo); } [Fact] public void Should_not_cache_downstream_route() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); _upstreamUrlPath = "/geoffistheworst/"; // Act WhenICreate(); GivenTheConfiguration(); _upstreamUrlPath = "/geoffisthebest/"; WhenICreateAgain(); // Assert _result.ShouldNotBe(_resultTwo); } [Fact] public void Should_create_downstream_route_with_no_path() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); _upstreamUrlPath = "/auth/"; // Act WhenICreate(); // Assert ThenTheDownstreamPathIsForwardSlash(); } [Fact] public void Should_create_downstream_route_with_only_first_segment_no_traling_slash() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); _upstreamUrlPath = "/auth"; // Act WhenICreate(); // Assert ThenTheDownstreamPathIsForwardSlash(); } [Fact] public void Should_create_downstream_route_with_segments_no_traling_slash() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); _upstreamUrlPath = "/auth/test"; // Act WhenICreate(); // Assert: Then the path does not have trailing slash var actual = _result.Data.Route.DownstreamRoute[0]; actual.DownstreamPathTemplate.Value.ShouldBe("/test"); actual.ServiceName.ShouldBe("auth"); actual.ServiceNamespace.ShouldBeEmpty(); actual.LoadBalancerKey.ShouldBe(".auth"); } [Fact] [Trait("Feat", "351")] [Trait("PR", "2324")] // This PR resolves the issue of forwarding the query string to the downstream when service discovery (dynamic routing), fixing a bug in the QoS Key construction for caching within the ResiliencePipelineRegistry. It now reuses the load balancing key to address the problem. public void Should_create_downstream_route_and_forward_query_string() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); const string queryString = "?test=1&best=2"; _upstreamUrlPath = "/auth/test" + queryString; // Act WhenICreate(); // Assert: Then the query string is removed var actual = _result.Data.Route.DownstreamRoute[0]; actual.DownstreamPathTemplate.Value.ShouldContain(queryString); // !!! actual.DownstreamPathTemplate.Value.ShouldBe("/test?test=1&best=2"); actual.ServiceName.ShouldBe("auth"); actual.ServiceNamespace.ShouldBeEmpty(); actual.LoadBalancerKey.ShouldBe(".auth"); } [Fact] public void Should_create_downstream_route_for_sticky_sessions() { // Arrange _loadBalancerOptions = new LoadBalancerOptions(nameof(CookieStickySessions), "boom", 1); GivenInternalConfiguration(); GivenTheConfiguration(); // Act WhenICreate(); // Assert var actual = _result.Data.Route.DownstreamRoute[0]; actual.LoadBalancerKey.ShouldBe("CookieStickySessions:boom"); actual.LoadBalancerOptions.Type.ShouldBe("CookieStickySessions"); actual.LoadBalancerOptions.ShouldBe(_loadBalancerOptions); } [Fact] public void Should_create_downstream_route_with_qos() { // Arrange _qoSOptions = new QoSOptions(1) { MinimumThroughput = 1, }; GivenInternalConfiguration(); GivenTheConfiguration(); // Act WhenICreate(); // Assert: Then the Qos options are set var actual = _result.Data.Route.DownstreamRoute[0]; actual.QosOptions.ShouldNotBeNull(); actual.QosOptions.UseQos.ShouldBeTrue(); } [Fact] public void Should_create_downstream_route_with_handler_options() { // Arrange GivenInternalConfiguration(); GivenTheConfiguration(); // Act WhenICreate(); // Assert: Then The Handler Options Are Set _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); } [Theory] [Trait("PR", "2324")] [InlineData("/service1", "service1", "")] [InlineData("/service2/", "service2", "")] [InlineData("/service3/bla", "service3", "")] [InlineData("/namespace1.service1", "service1", "namespace1")] [InlineData("/namespace2.service2/", "service2", "namespace2")] [InlineData("/namespace3.service3/bla-bla", "service3", "namespace3")] [InlineData("/namespace4.service.4/ha-a", "service.4", "namespace4")] [InlineData("/name.space5.service5/ha-ha", "space5.service5", "name")] public void GetServiceName(string urlPath, string expected, string expectedNamespace) { var method = _finder.GetType().GetMethod(nameof(GetServiceName), BindingFlags.Instance | BindingFlags.NonPublic); object[] parameters = [urlPath, null]; // Act string actual = (string)method.Invoke(_finder, parameters); string actualNamespace = (string)parameters[1]; Assert.Equal(expected, actual); Assert.Equal(expectedNamespace, actualNamespace); } [Fact] [Trait("Feat", "585")] [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 public void Should_create_downstream_route_with_load_balancer_options() { // Arrange var lbOptions = new LoadBalancerOptions("testBalancer", "testKey", 3); var downstreamRoute = new DownstreamRouteBuilder() .WithServiceName("auth") .WithLoadBalancerOptions(lbOptions) .WithLoadBalancerKey("|auth") .WithMetadata(_metadataOptions) .WithRateLimitOptions(_rateLimitOptions) .WithQosOptions(_qoSOptions) .WithHttpHandlerOptions(_handlerOptions) .WithDownstreamScheme("http") .Build(); var route = new Route(true, downstreamRoute); // create dynamic route GivenInternalConfiguration(route); GivenTheConfiguration(); // Act WhenICreate(); // Assert ThenTheDownstreamRouteIsCreated(lbType: "testBalancer", lbKey: "|auth"); var downstream = _result.Data.Route.DownstreamRoute[0]; downstream.LoadBalancerOptions.ShouldNotBeNull(); downstream.LoadBalancerOptions.Type.ShouldBe("testBalancer"); downstream.LoadBalancerOptions.Key.ShouldBe("testKey"); downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 [InlineData(false)] [InlineData(true)] public void ShouldFindFirstOrDefaultDownstreamRoute_WithOrWithoutServiceNamespace(bool hasNamespace) { // Arrange var lbOptions = new LoadBalancerOptions("testBalancer", "testKey", 3); var dRoute1 = new DownstreamRouteBuilder() .WithServiceName("service1") .Build(); var dRoute2 = new DownstreamRouteBuilder() .WithServiceName("service2") .WithServiceNamespace(hasNamespace ? "namespace2" : string.Empty) .WithLoadBalancerKey("namespace2-service2") .WithLoadBalancerOptions(lbOptions) .WithQosOptions(_qoSOptions) .WithHttpHandlerOptions(_handlerOptions) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); var route = new Route(true) { DownstreamRoute = [dRoute1, dRoute2], }; GivenInternalConfiguration(route, 1); GivenTheConfiguration(); _upstreamUrlPath = hasNamespace ? $"/{dRoute2.ServiceNamespace}.{dRoute2.ServiceName}/test" : $"/{dRoute2.ServiceName}/test"; // Act WhenICreate(); // Assert ThenTheDownstreamRouteIsCreated("service2", hasNamespace ? "namespace2" : "", "testBalancer", "namespace2-service2"); var downstream = _result.Data.Route.DownstreamRoute[0]; downstream.LoadBalancerOptions.ShouldNotBeNull(); downstream.LoadBalancerOptions.Type.ShouldBe("testBalancer"); downstream.LoadBalancerOptions.Key.ShouldBe("testKey"); downstream.LoadBalancerOptions.ExpiryInMs.ShouldBe(3); } private void ThenTheDownstreamRouteIsCreated(string serviceName = null, string serviceNamespace = null, string lbType = null, string lbKey = null) { _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/test"); _result.Data.Route.UpstreamHttpMethod.ShouldContain(HttpMethod.Get); _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe(serviceName ?? "auth"); _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBe(serviceNamespace ?? string.Empty); _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(lbKey ?? ".auth"); _result.Data.Route.DownstreamRoute[0].UseServiceDiscovery.ShouldBeTrue(); _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldNotBeNull(); _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); _result.Data.Route.DownstreamRoute[0].DownstreamScheme.ShouldBe("http"); _result.Data.Route.DownstreamRoute[0].LoadBalancerOptions.Type.ShouldBe(lbType ?? nameof(NoLoadBalancer)); _result.Data.Route.DownstreamRoute[0].HttpHandlerOptions.ShouldBe(_handlerOptions); _result.Data.Route.DownstreamRoute[0].QosOptions.ShouldNotBeNull(); _result.Data.Route.UpstreamTemplatePattern.ShouldNotBeNull(); _result.Data.Route.DownstreamRoute[0].UpstreamPathTemplate.ShouldNotBeNull(); var kv = _upstreamHeaders.First(); _result.Data.Route.UpstreamHeaderTemplates.ShouldNotBeNull() .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); _result.Data.Route.DownstreamRoute[0].UpstreamHeaders.ShouldNotBeNull() .FirstOrDefault(x => x.Key == kv.Key).Value.Template.ShouldBe(kv.Value); } private void ThenTheDownstreamPathIsForwardSlash() { _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe("/"); _result.Data.Route.DownstreamRoute[0].ServiceName.ShouldBe("auth"); _result.Data.Route.DownstreamRoute[0].ServiceNamespace.ShouldBeEmpty(); _result.Data.Route.DownstreamRoute[0].LoadBalancerKey.ShouldBe(".auth"); } private void GivenTheConfiguration() { _upstreamHost = "doesnt matter"; _upstreamUrlPath = "/auth/test"; _upstreamHttpMethod = "GET"; _upstreamHeaders = new HeaderDictionary() { { "testHeader", "testHeaderValue" }, }; var kv = _upstreamHeaders.First(); _upstreamHeaderTemplatePatternCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())) .Returns(new Dictionary() { { kv.Key, new(kv.Value, kv.Value) }, }); } private void GivenInternalConfiguration(Route route = null, int index = 0) { var dr = route?.DownstreamRoute[index]; _configuration = new InternalConfiguration(route is null ? null : [route]) { AdministrationPath = "/AdminPath", RequestId = "requestID", LoadBalancerOptions = dr?.LoadBalancerOptions ?? _loadBalancerOptions, DownstreamScheme = (dr?.DownstreamScheme).IfEmpty(Uri.UriSchemeHttp), QoSOptions = dr?.QosOptions ?? _qoSOptions, HttpHandlerOptions = dr?.HttpHandlerOptions ?? _handlerOptions, DownstreamHttpVersion = dr?.DownstreamHttpVersion ?? new Version("1.1"), DownstreamHttpVersionPolicy = dr?.DownstreamHttpVersionPolicy ?? HttpVersionPolicy.RequestVersionOrLower, MetadataOptions = dr?.MetadataOptions ?? _metadataOptions, RateLimitOptions = dr?.RateLimitOptions ?? _rateLimitOptions, Timeout = dr?.Timeout ?? 111, }; } private void WhenICreate() { _result = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } private void WhenICreateAgain() { _resultTwo = _finder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _configuration, _upstreamHost, _upstreamHeaders); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; namespace Ocelot.UnitTests.DownstreamRouteFinder; public class DownstreamRouteFinderMiddlewareTests : UnitTest { private readonly Mock _finder; private readonly Mock _factory; private Response _downstreamRoute; private IInternalConfiguration _config; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly DownstreamRouteFinderMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public DownstreamRouteFinderMiddlewareTests() { _httpContext = new DefaultHttpContext(); _finder = new Mock(); _factory = new Mock(); _factory.Setup(x => x.Get(It.IsAny())).Returns(_finder.Object); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new DownstreamRouteFinderMiddleware(_next, _loggerFactory.Object, _factory.Object); } [Fact] public async Task Should_call_scoped_data_repository_correctly() { // Arrange var config = new InternalConfiguration(); var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .Build(); GivenTheDownStreamRouteFinderReturns(new( new List(), new Route(downstreamRoute, HttpMethod.Get))); GivenTheFollowingConfig(config); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheScopedDataRepositoryIsCalledCorrectly(); } private void GivenTheFollowingConfig(IInternalConfiguration config) { _config = config; _httpContext.Items.SetIInternalConfiguration(config); } private void GivenTheDownStreamRouteFinderReturns(DownstreamRouteHolder downstreamRoute) { _downstreamRoute = new OkResponse(downstreamRoute); _finder .Setup(x => x.Get(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_downstreamRoute); } private void ThenTheScopedDataRepositoryIsCalledCorrectly() { _httpContext.Items.TemplatePlaceholderNameAndValues().ShouldBe(_downstreamRoute.Data.TemplatePlaceholderNameAndValues); _httpContext.Items.IInternalConfiguration().ServiceProviderConfiguration.ShouldBe(_config.ServiceProviderConfiguration); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteFinderTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; using Ocelot.Values; using _DownstreamRouteFinder_ = Ocelot.DownstreamRouteFinder.Finder.DownstreamRouteFinder; namespace Ocelot.UnitTests.DownstreamRouteFinder; public class DownstreamRouteFinderTests : UnitTest { private readonly _DownstreamRouteFinder_ _routeFinder; private readonly Mock _mockUrlMatcher; private readonly Mock _mockHeadersMatcher; private readonly Mock _urlPlaceholderFinder; private readonly Mock _headerPlaceholderFinder; private string _upstreamUrlPath; private Response _result; private List _routesConfig; private InternalConfiguration _config; private UrlMatch _match; private string _upstreamHttpMethod; private string _upstreamHost; private IHeaderDictionary _upstreamHeaders; private string _upstreamQuery; public DownstreamRouteFinderTests() { _mockUrlMatcher = new Mock(); _mockHeadersMatcher = new Mock(); _urlPlaceholderFinder = new Mock(); _headerPlaceholderFinder = new Mock(); _routeFinder = new _DownstreamRouteFinder_(_mockUrlMatcher.Object, _urlPlaceholderFinder.Object, _mockHeadersMatcher.Object, _headerPlaceholderFinder.Object); } [Fact] public void Should_return_highest_priority_when_first() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); var expectedRoute = GivenRoute(method: "Post", priority: 1); _routesConfig = new() { expectedRoute, GivenRoute(method: "Post", priority: 0), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), expectedRoute)); } [Fact] public void Should_return_highest_priority_when_lowest() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); var expectedRoute = GivenRoute(method: "Post", priority: 1); _routesConfig = new() { GivenRoute(method: "Post", priority: 0), expectedRoute, }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), expectedRoute)); } [Fact] public void Should_return_route() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } [Fact] public void Should_not_append_slash_to_upstream_url_path() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); // Assert: Then The Url Matcher Is Called Correctly _mockUrlMatcher.Verify(x => x.Match("matchInUrlMatcher", _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } [Fact] public void Should_return_route_if_upstream_path_and_upstream_template_are_the_same() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); } [Fact] public void Should_return_correct_route_for_http_verb() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(downstream: "someDownstreamPath", method: "Get", priority: 1), GivenRoute(downstream: "someDownstreamPathForAPost", method: "Post", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(downstream: "someDownstreamPathForAPost", method: "Post", priority: 1) )); } [Fact] public void Should_not_return_route() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "dontMatchPath/"; _upstreamQuery = string.Empty; _routesConfig = new List { GivenRoute(downstream: "somPath", upstream: "somePath", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(false)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert _result.IsError.ShouldBeTrue(); ThenTheUrlMatcherIsCalledCorrectly(); } [Fact] public void Should_return_correct_route_for_http_verb_setting_multiple_upstream_http_method() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(upstreamMethods: ["Get", "Post"], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(upstreamMethods: ["Post"], priority: 1))); } [Fact] public void Should_return_correct_route_for_http_verb_setting_all_upstream_http_method() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(upstreamMethods: [], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(upstreamMethods: ["Post"], priority: 1))); } [Fact] public void Should_not_return_route_for_http_verb_not_setting_in_upstream_http_method() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "someUpstreamPath"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(upstreamMethods: ["Get", "Patch", "Delete"], priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Post"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert _result.IsError.ShouldBeTrue(); ThenTheUrlMatcherIsNotCalled(); } [Fact] public void Should_return_route_when_host_matches() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "MATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(host: "MATCH", priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } [Fact] public void Should_return_route_when_upstreamhost_is_null() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "MATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(host: null, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } [Fact] public void Should_not_return_route_when_host_doesnt_match() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "DONTMATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(host: "MATCH", upstreamMethods: ["Get"], priority: 1), GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert _result.IsError.ShouldBeTrue(); ThenTheUrlMatcherIsNotCalled(); } [Fact] public void Should_not_return_route_when_host_doesnt_match_with_empty_upstream_http_method() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "DONTMATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert _result.IsError.ShouldBeTrue(); ThenTheUrlMatcherIsNotCalled(); } [Fact] public void Should_return_route_when_host_does_match_with_empty_upstream_http_method() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "MATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(host: "MATCH", upstreamMethods: [], priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheUrlMatcherIsCalledCorrectly(1, 0); } [Fact] public void Should_return_route_when_host_matches_but_null_host_on_same_path_first() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHost = "MATCH"; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(downstream: "THENULLPATH", priority: 1), GivenRoute(host: "MATCH", priority: 1), // empty list of methods }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( new List(), GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(1, 0); ThenTheUrlMatcherIsCalledCorrectly(1, 1); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public void Should_return_route_when_upstream_headers_match() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); var upstreamHeaders = new HeaderDictionary() { ["header1"] = "headerValue1", ["header2"] = "headerValue2", ["header3"] = "headerValue3", }; var upstreamHeadersConfig = new Dictionary() { ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), }; var urlPlaceholders = new List { new("url", "urlValue") }; var headerPlaceholders = new List { new("header", "headerValue") }; _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHeaders = upstreamHeaders; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(urlPlaceholders)); GivenTheHeaderPlaceholderAndNameFinderReturns(headerPlaceholders); _routesConfig = new() { GivenRoute(headers: upstreamHeadersConfig, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert ThenTheFollowingIsReturned(new( urlPlaceholders.Union(headerPlaceholders).ToList(), GivenRoute(priority: 1))); ThenTheUrlMatcherIsCalledCorrectly(); } [Fact] [Trait("PR", "1312")] [Trait("Feat", "360")] public void Should_not_return_route_when_upstream_headers_dont_match() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); var upstreamHeadersConfig = new Dictionary() { ["header1"] = new UpstreamHeaderTemplate("headerValue1", "headerValue1"), ["header2"] = new UpstreamHeaderTemplate("headerValue2", "headerValue2"), }; _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; _upstreamHeaders = new HeaderDictionary() { { "header1", "headerValue1" } }; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new List())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(headers: upstreamHeadersConfig, priority: 1), GivenRoute(headers: upstreamHeadersConfig, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(false); _upstreamHttpMethod = "Get"; // Act _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); // Assert _result.IsError.ShouldBeTrue(); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "2319")] // https://github.com/ThreeMammals/Ocelot/pull/2324 [InlineData(false)] [InlineData(true)] public void Should_filter_static_routes(bool isDynamic) { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().Build(); _upstreamUrlPath = "matchInUrlMatcher/"; _upstreamQuery = string.Empty; GivenTheTemplateVariableAndNameFinderReturns(new OkResponse>(new())); GivenTheHeaderPlaceholderAndNameFinderReturns(new List()); _routesConfig = new() { GivenRoute(priority: 1), GivenRoute(isDynamic: isDynamic, priority: 1), }; GivenTheConfigurationIs(string.Empty, serviceProviderConfig); GivenTheUrlMatcherReturns(new UrlMatch(true)); GivenTheHeadersMatcherReturns(true); _upstreamHttpMethod = "Get"; // Act, Assert _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); _result.Data.Route.IsDynamic.ShouldBeFalse(); // Act, Assert 2 _routesConfig.RemoveAll(r => !r.IsDynamic); // remove all static routes GivenTheConfigurationIs(string.Empty, serviceProviderConfig); _result = _routeFinder.Get(_upstreamUrlPath, _upstreamQuery, _upstreamHttpMethod, _config, _upstreamHost, _upstreamHeaders); _result.IsError.ShouldBeTrue(); } private static Route GivenRoute(bool? isDynamic = null, string downstream = null, List upstreamMethods = null, string method = null, UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null, string host = null, IDictionary headers = null) { var route = GivenDownstreamRoute(downstream, upstreamMethods, method, upTemplate, upstream, priority); upstream ??= "someUpstreamPath"; upTemplate ??= new(upstream, priority ?? 1, false, upstream); upstreamMethods ??= [method ?? HttpMethods.Get]; return new(isDynamic ?? false) { DownstreamRoute = [route], UpstreamHttpMethod = upstreamMethods.Select(m => new HttpMethod(m)).ToHashSet(), UpstreamTemplatePattern = upTemplate, UpstreamHost = host, UpstreamHeaderTemplates = headers, }; } private static DownstreamRoute GivenDownstreamRoute(string downstream = null, List upstreamMethods = null, string method = null, UpstreamPathTemplate upTemplate = null, string upstream = null, int? priority = null) => new DownstreamRouteBuilder() .WithDownstreamPathTemplate(downstream ?? "someDownstreamPath") .WithUpstreamHttpMethod(upstreamMethods ?? [method ?? HttpMethods.Get]) .WithUpstreamPathTemplate(upTemplate ?? new(upstream ?? "someUpstreamPath", priority ?? 1, false, upstream ?? "someUpstreamPath")) .Build(); private void GivenTheTemplateVariableAndNameFinderReturns(Response> response) { _urlPlaceholderFinder .Setup(x => x.Find(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(response); } private void GivenTheHeaderPlaceholderAndNameFinderReturns(List placeholders) { _headerPlaceholderFinder .Setup(x => x.Find(It.IsAny(), It.IsAny>())) .Returns(placeholders); } private void ThenTheUrlMatcherIsCalledCorrectly() { _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Once); } private void ThenTheUrlMatcherIsCalledCorrectly(int times, int index = 0) { _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[index].UpstreamTemplatePattern), Times.Exactly(times)); } private void ThenTheUrlMatcherIsNotCalled() { _mockUrlMatcher .Verify(x => x.Match(_upstreamUrlPath, _upstreamQuery, _routesConfig[0].UpstreamTemplatePattern), Times.Never); } private void GivenTheUrlMatcherReturns(UrlMatch match) { _match = match; _mockUrlMatcher .Setup(x => x.Match(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(_match); } private void GivenTheHeadersMatcherReturns(bool headersMatch) { _mockHeadersMatcher .Setup(x => x.Match(It.IsAny(), It.IsAny>())) .Returns(headersMatch); } private void GivenTheConfigurationIs(string adminPath, ServiceProviderConfiguration serviceProviderConfig) { _config = new InternalConfiguration(_routesConfig.ToArray()) { AdministrationPath = adminPath, DownstreamHttpVersion = new Version("1.1"), DownstreamHttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower, HttpHandlerOptions = new(), LoadBalancerOptions = new(), QoSOptions = new(), ServiceProviderConfiguration = serviceProviderConfig, }; } private void ThenTheFollowingIsReturned(DownstreamRouteHolder expected) { _result.Data.Route.DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.Route.DownstreamRoute[0].DownstreamPathTemplate.Value); _result.Data.Route.UpstreamTemplatePattern.Priority.ShouldBe(expected.Route.UpstreamTemplatePattern.Priority); for (var i = 0; i < _result.Data.TemplatePlaceholderNameAndValues.Count; i++) { _result.Data.TemplatePlaceholderNameAndValues[i].Name.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Name); _result.Data.TemplatePlaceholderNameAndValues[i].Value.ShouldBe(expected.TemplatePlaceholderNameAndValues[i].Value); } _result.IsError.ShouldBeFalse(); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteHolderTests.cs ================================================ using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; namespace Ocelot.UnitTests.DownstreamRouteFinder; public class DownstreamRouteHolderTests { [Fact] public void Ctor() { // Arrange, Act DownstreamRouteHolder holder = new(); Assert.Null(holder.Route); Assert.Null(holder.TemplatePlaceholderNameAndValues); } [Fact] public void Ctor_List_Route() { // Arrange Route route = new(); List placeholders = new(); // Act DownstreamRouteHolder holder = new(placeholders, route); // Assert Assert.Equal(route, holder.Route); Assert.Equal(placeholders, holder.TemplatePlaceholderNameAndValues); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/DownstreamRouteProviderFactoryTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; namespace Ocelot.UnitTests.DownstreamRouteFinder; using Ocelot.DependencyInjection; using Ocelot.DownstreamRouteFinder.Finder; public class DownstreamRouteProviderFactoryTests : UnitTest { private readonly DownstreamRouteProviderFactory _factory; private IInternalConfiguration _config; private IDownstreamRouteProvider _result; private readonly Mock _logger; private readonly Mock _loggerFactory; public DownstreamRouteProviderFactoryTests() { var services = new ServiceCollection(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); Features.AddOcelotHeaderRouting(services); // AddSingleton() var provider = services.BuildServiceProvider(true); _logger = new Mock(); _loggerFactory = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _factory = new DownstreamRouteProviderFactory(provider, _loggerFactory.Object); } [Fact] public void Should_return_downstream_route_finder() { // Arrange var route = new Route(); GivenTheRoutes(route); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_finder_when_not_dynamic_re_route_and_service_discovery_on() { // Arrange var route = new Route() { UpstreamTemplatePattern = new UpstreamPathTemplateBuilder().WithOriginalValue("woot").Build(), }; var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); GivenTheRoutes(route, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_finder_as_no_service_discovery_given_no_scheme() { // Arrange var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme(string.Empty).WithHost("test").WithPort(50).Build(); GivenTheRoutes(null, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_finder_as_no_service_discovery_given_no_host() { // Arrange var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost(string.Empty).WithPort(50).Build(); GivenTheRoutes(null, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_finder_given_no_service_discovery_port() { // Arrange var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("localhost").WithPort(0).Build(); GivenTheRoutes(null, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_finder_given_no_service_discovery_type() { // Arrange var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("localhost").WithPort(50).WithType(string.Empty).Build(); GivenTheRoutes(null, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_creator() { // Arrange var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); GivenTheRoutes(null, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } [Fact] public void Should_return_downstream_route_creator_with_dynamic_re_route() { // Arrange var route = new Route(); var spConfig = new ServiceProviderConfigurationBuilder() .WithScheme("http").WithHost("test").WithPort(50).WithType("test").Build(); GivenTheRoutes(route, spConfig); // Act _result = _factory.Get(_config); // Assert _result.ShouldBeOfType(); } private void GivenTheRoutes(Route route, ServiceProviderConfiguration config = null) { Route[] routes = route == null ? Array.Empty() : [route]; _config = new InternalConfiguration(routes) { ServiceProviderConfiguration = config, }; } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeaderPlaceholderNameAndValueFinderTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; [Trait("PR", "1312")] [Trait("Feat", "360")] public class HeaderPlaceholderNameAndValueFinderTests : UnitTest { private readonly HeaderPlaceholderNameAndValueFinder _finder = new(); [Fact] public void Should_return_no_placeholders() { // Arrange var upstreamHeaderTemplates = new Dictionary(); var upstreamHeaders = new HeaderDictionary(); var expected = new List(); // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_one_placeholder_with_value_when_no_other_text() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), }; var upstreamHeaders = new HeaderDictionary() { ["country"] = "PL", }; var expected = new List { new("{countrycode}", "PL"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_one_placeholder_with_value_when_other_text_on_the_right() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["country"] = new("^(?.+)-V1$", "{header:countrycode}-V1"), }; var upstreamHeaders = new HeaderDictionary() { ["country"] = "PL-V1", }; var expected = new List { new("{countrycode}", "PL"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_one_placeholder_with_value_when_other_text_on_the_left() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["country"] = new("^V1-(?.+)$", "V1-{header:countrycode}"), }; var upstreamHeaders = new HeaderDictionary() { ["country"] = "V1-PL", }; var expected = new List { new("{countrycode}", "PL"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_one_placeholder_with_value_when_other_texts_surrounding() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["country"] = new("^cc:(?.+)-V1$", "cc:{header:countrycode}-V1"), }; var upstreamHeaders = new HeaderDictionary() { ["country"] = "cc:PL-V1", }; var expected = new List { new("{countrycode}", "PL"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_two_placeholders_with_text_between() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["countryAndVersion"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), }; var upstreamHeaders = new HeaderDictionary() { ["countryAndVersion"] = "PL-v1", }; var expected = new List { new("{countrycode}", "PL"), new("{version}", "v1"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } [Fact] public void Should_return_placeholders_from_different_headers() { // Arrange var upstreamHeaderTemplates = new Dictionary { ["country"] = new("^(?i)(?.+)$", "{header:countrycode}"), ["version"] = new("^(?i)(?.+)$", "{header:version}"), }; var upstreamHeaders = new HeaderDictionary() { ["country"] = "PL", ["version"] = "v1", }; var expected = new List { new("{countrycode}", "PL"), new("{version}", "v1"), }; // Act var result = _finder.Find(upstreamHeaders, upstreamHeaderTemplates).ToList(); // Assert TheResultIs(result, expected); } private static void TheResultIs(List actual, List expected) { actual.ShouldNotBeNull(); actual.Count.ShouldBe(expected.Count); actual.ForEach(x => expected.Any(e => e.Name == x.Name && e.Value == x.Value).ShouldBeTrue()); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/HeaderMatcher/HeadersToHeaderTemplatesMatcherTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.HeaderMatcher; using Ocelot.Values; namespace Ocelot.UnitTests.DownstreamRouteFinder.HeaderMatcher; [Trait("PR", "1312")] [Trait("Feat", "360")] public class HeadersToHeaderTemplatesMatcherTests : UnitTest { private readonly HeadersToHeaderTemplatesMatcher _matcher = new(); [Fact] public void Should_match_when_no_template_headers() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "anyHeaderValue", }; var templateHeaders = new Dictionary(); // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_match_the_same_headers() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "anyHeaderValue", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_not_match_the_same_headers_when_differ_case_and_case_sensitive() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "ANYHEADERVALUE", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeFalse(); } [Fact] public void Should_match_the_same_headers_when_differ_case_and_case_insensitive() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "ANYHEADERVALUE", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_not_match_different_headers_values() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "anyHeaderValueDifferent", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeFalse(); } [Fact] public void Should_not_match_the_same_headers_names() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeaderDifferent"] = "anyHeaderValue", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeFalse(); } [Fact] public void Should_match_all_the_same_headers() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "anyHeaderValue", ["notNeededHeader"] = "notNeededHeaderValue", ["secondHeader"] = "secondHeaderValue", ["thirdHeader"] = "thirdHeaderValue", }; var templateHeaders = new Dictionary() { ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_not_match_the_headers_when_one_of_them_different() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "anyHeaderValue", ["notNeededHeader"] = "notNeededHeaderValue", ["secondHeader"] = "secondHeaderValueDIFFERENT", ["thirdHeader"] = "thirdHeaderValue", }; var templateHeaders = new Dictionary() { ["secondHeader"] = new("^(?i)secondHeaderValue$", "secondHeaderValue"), ["thirdHeader"] = new("^(?i)thirdHeaderValue$", "thirdHeaderValue"), ["anyHeader"] = new("^(?i)anyHeaderValue$", "anyHeaderValue"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeFalse(); } [Fact] public void Should_match_the_header_with_placeholder() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "PL", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)(?.+)$", "{header:countrycode}"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_match_the_header_with_placeholders() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "PL-V1", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeTrue(); } [Fact] public void Should_not_match_the_header_with_placeholders() { // Arrange var upstreamHeaders = new HeaderDictionary() { ["anyHeader"] = "PL", }; var templateHeaders = new Dictionary() { ["anyHeader"] = new("^(?i)(?.+)-(?.+)$", "{header:countrycode}-{header:version}"), }; // Act var result = _matcher.Match(upstreamHeaders, templateHeaders); // Assert result.ShouldBeFalse(); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/PlaceholderNameAndValueTests.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher; public sealed class PlaceholderNameAndValueTests { [Fact] public void Key() { // Arrange PlaceholderNameAndValue placeholder = new("{test}", "testing"); // Act var actual = placeholder.Key; // Assert Assert.Equal("testing", placeholder.Value); Assert.Equal("{test}", placeholder.Name); Assert.Equal("test", actual); } [Fact] public void ToString_Override() { // Arrange PlaceholderNameAndValue placeholder = new("{test}", "testing"); // Act var actual = placeholder.ToString(); // Assert Assert.Equal("testing", placeholder.Value); Assert.Equal("{test}", placeholder.Name); Assert.Equal("[{test}=testing]", actual); } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/RegExUrlMatcherTests.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Values; using System.Text.RegularExpressions; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher; public class RegExUrlMatcherTests : UnitTest { private readonly RegExUrlMatcher _matcher = new(); private static readonly string Empty = string.Empty; [Fact] public void Should_not_match() { // Arrange const string path = "/api/v1/aaaaaaaaa/cards"; const string downstreamPathTemplate = "^(?i)/api/v[^/]+/cards$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } [Fact] public void Should_match() { // Arrange const string path = "/api/v1/cards"; const string downstreamPathTemplate = "^(?i)/api/v[^/]+/cards$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_match_path_with_no_query_string() { // Arrange const string regExForwardSlashAndOnePlaceHolder = "^(?i)/newThing$"; const string path = "/newThing"; const string queryString = "?DeviceType=IphoneApp&Browser=moonpigIphone&BrowserString=-&CountryCode=123&DeviceName=iPhone 5 (GSM+CDMA)&OperatingSystem=iPhone OS 7.1.2&BrowserVersion=3708AdHoc&ipAddress=-"; const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, queryString, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_match_query_string() { // Arrange const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+$"; const string path = "/api/subscriptions/1/updates"; const string queryString = "?unitId=2"; const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder; const bool containsQueryString = true; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate, containsQueryString); // Act var result = _matcher.Match(path, queryString, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_match_query_string_with_multiple_params() { // Arrange const string regExForwardSlashAndOnePlaceHolder = "^(?i)/api/subscriptions/[^/]+/updates\\?unitId=.+&productId=.+$"; const string path = "/api/subscriptions/1/updates?unitId=2"; const string queryString = "?unitId=2&productId=2"; const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder; const bool containsQueryString = true; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate, containsQueryString); // Act var result = _matcher.Match(path, queryString, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_not_match_slash_becaue_we_need_to_match_something_after_it() { // Arrange const string regExForwardSlashAndOnePlaceHolder = "^/[0-9a-zA-Z].+"; const string path = "/"; const string downstreamPathTemplate = regExForwardSlashAndOnePlaceHolder; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } [Fact] public void Should_not_match_forward_slash_only_regex() { // Arrange const string path = "/working/"; const string downstreamPathTemplate = "^/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } [Fact] public void Should_not_match_issue_134() { // Arrange const string path = "/api/vacancy/1/"; const string downstreamPathTemplate = "^(?i)/vacancy/[^/]+/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } [Fact] public void Should_match_forward_slash_only_regex() { // Arrange const string path = "/"; const string downstreamPathTemplate = "^/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_find_match_when_template_smaller_than_valid_path() { // Arrange const string path = "/api/products/2354325435624623464235"; const string downstreamPathTemplate = "^/api/products/.+$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_not_find_match() { // Arrange const string path = "/api/values"; const string downstreamPathTemplate = "^/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } [Fact] public void Can_match_down_stream_url() { // Arrange const string path = ""; const string downstreamPathTemplate = "^$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_no_slash() { // Arrange const string path = "api"; const string downstreamPathTemplate = "^api$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_one_slash() { // Arrange const string path = "api/"; const string downstreamPathTemplate = "^api/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template() { // Arrange const string path = "api/product/products/"; const string downstreamPathTemplate = "^api/product/products/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_one_place_holder() { // Arrange const string path = "api/product/products/1"; const string downstreamPathTemplate = "^api/product/products/.+$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders() { // Arrange const string path = "api/product/products/1/2"; const string downstreamPathTemplate = "^api/product/products/[^/]+/.+$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() { // Arrange const string path = "api/product/products/1/categories/2"; const string downstreamPathTemplate = "^api/product/products/[^/]+/categories/.+$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() { // Arrange const string path = "api/product/products/1/categories/2/variant/123"; const string downstreamPathTemplate = "^api/product/products/[^/]+/categories/[^/]+/variant/.+$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders() { // Arrange const string path = "api/product/products/1/categories/2/variant/"; const string downstreamPathTemplate = "^api/product/products/[^/]+/categories/[^/]+/variant/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_ignore_case_sensitivity() { // Arrange const string path = "API/product/products/1/categories/2/variant/"; const string downstreamPathTemplate = "^(?i)api/product/products/[^/]+/categories/[^/]+/variant/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeTrue(); } [Fact] public void Should_respect_case_sensitivity() { // Arrange const string path = "API/product/products/1/categories/2/variant/"; const string downstreamPathTemplate = "^api/product/products/[^/]+/categories/[^/]+/variant/$"; var upt = GivenUpstreamPathTemplate(downstreamPathTemplate); // Act var result = _matcher.Match(path, Empty, upt); // Assert result.Match.ShouldBeFalse(); } private static UpstreamPathTemplate GivenUpstreamPathTemplate(string downstreamPathTemplate, bool containsQueryString = false) => new(downstreamPathTemplate, 0, containsQueryString, downstreamPathTemplate) { Pattern = new Regex(downstreamPathTemplate), }; } ================================================ FILE: test/Ocelot.UnitTests/DownstreamRouteFinder/UrlMatcher/UrlPathPlaceholderNameAndValueFinderTests.cs ================================================ using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Responses; namespace Ocelot.UnitTests.DownstreamRouteFinder.UrlMatcher; public class UrlPathPlaceholderNameAndValueFinderTests : UnitTest { private readonly UrlPathPlaceholderNameAndValueFinder _finder = new(); private Response> _result; private static readonly string Empty = string.Empty; [Fact] public void Can_match_down_stream_url() { // Arrange, Act _result = _finder.Find(Empty, Empty, Empty); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Can_match_down_stream_url_with_nothing_then_placeholder_no_value_is_blank() { // Arrange, Act _result = _finder.Find(Empty, Empty, "/{url}"); // Assert ThenSinglePlaceholderIs("{url}", Empty); } [Fact] public void Can_match_down_stream_url_with_nothing_then_placeholder_value_is_test() { // Arrange, Act _result = _finder.Find("/test", Empty, "/{url}"); // Assert ThenSinglePlaceholderIs("{url}", "test"); } [Fact] public void Should_match_everything_in_path_with_query() { // Arrange, Act _result = _finder.Find("/test/toot", "?$filter=Name%20eq%20'Sam'", "/{everything}"); // Assert ThenSinglePlaceholderIs("{everything}", "test/toot"); } [Fact] public void Should_match_everything_in_path() { // Arrange, Act _result = _finder.Find("/test/toot", Empty, "/{everything}"); // Assert ThenSinglePlaceholderIs("{everything}", "test/toot"); } [Fact] public void Can_match_down_stream_url_with_forward_slash_then_placeholder_no_value_is_blank() { // Arrange, Act _result = _finder.Find("/", Empty, "/{url}"); // Assert ThenSinglePlaceholderIs("{url}", Empty); } [Fact] public void Can_match_down_stream_url_with_forward_slash() { // Arrange, Act _result = _finder.Find("/", Empty, "/"); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Can_match_down_stream_url_with_forward_slash_then_placeholder_then_another_value() { // Arrange, Act _result = _finder.Find("/1/products", Empty, "/{url}/products"); // Assert ThenSinglePlaceholderIs("{url}", "1"); } [Fact] public void Should_not_find_anything() { // Arrange, Act _result = _finder.Find("/products", Empty, "/products/"); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Should_find_query_string() { // Arrange, Act _result = _finder.Find("/products", "?productId=1", "/products?productId={productId}"); // Assert ThenSinglePlaceholderIs("{productId}", "1"); } [Fact] public void Should_find_query_string_dont_include_hardcoded() { // Arrange, Act _result = _finder.Find("/products", "?productId=1&categoryId=2", "/products?productId={productId}"); // Assert ThenSinglePlaceholderIs("{productId}", "1"); } [Fact] public void Should_find_multiple_query_string() { // Arrange, Act _result = _finder.Find("/products", "?productId=1&categoryId=2", "/products?productId={productId}&categoryId={categoryId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2")); } [Fact] public void Should_find_multiple_query_string_and_path() { // Arrange, Act _result = _finder.Find("/products/3", "?productId=1&categoryId=2", "/products/{account}?productId={productId}&categoryId={categoryId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2"), new("{account}", "3")); } [Fact] public void Should_find_multiple_query_string_and_path_that_ends_with_slash() { // Arrange, Act _result = _finder.Find("/products/3/", "?productId=1&categoryId=2", "/products/{account}/?productId={productId}&categoryId={categoryId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2"), new("{account}", "3")); } [Fact] public void Can_match_down_stream_url_with_no_slash() { // Arrange, Act _result = _finder.Find("api", Empty, "api"); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Can_match_down_stream_url_with_one_slash() { // Arrange, Act _result = _finder.Find("api/", Empty, "api/"); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Can_match_down_stream_url_with_downstream_template() { // Arrange, Act _result = _finder.Find("api/product/products/", Empty, "api/product/products/"); // Assert ThenTheTemplatesVariablesAre(); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_one_place_holder() { // Arrange, Act _result = _finder.Find("api/product/products/1", Empty, "api/product/products/{productId}"); // Assert ThenSinglePlaceholderIs("{productId}", "1"); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders() { // Arrange, Act _result = _finder.Find("api/product/products/1/2", Empty, "api/product/products/{productId}/{categoryId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2")); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_two_place_holders_seperated_by_something() { // Arrange, Act _result = _finder.Find("api/product/products/1/categories/2", Empty, "api/product/products/{productId}/categories/{categoryId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2")); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders_seperated_by_something() { // Arrange, Act _result = _finder.Find("api/product/products/1/categories/2/variant/123", Empty, "api/product/products/{productId}/categories/{categoryId}/variant/{variantId}"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2"), new("{variantId}", "123")); } [Fact] public void Can_match_down_stream_url_with_downstream_template_with_three_place_holders() { // Arrange, Act _result = _finder.Find("api/product/products/1/categories/2/variant/", Empty, "api/product/products/{productId}/categories/{categoryId}/variant/"); // Assert ThenTheTemplatesVariablesAre( new("{productId}", "1"), new("{categoryId}", "2")); } [Theory] [Trait("Feat", "89")] [InlineData("/api/{finalUrlPath}", "/api/product/products/categories/", "{finalUrlPath}", "product/products/categories/")] [InlineData("/myApp1Name/api/{urlPath}", "/myApp1Name/api/products/1", "{urlPath}", "products/1")] public void Can_match_down_stream_url_with_downstream_template_with_place_holder_to_final_url_path(string template, string path, string placeholderName, string placeholderValue) { // Arrange, Act _result = _finder.Find(path, Empty, template); // Assert ThenSinglePlaceholderIs(placeholderName, placeholderValue); } [Fact] [Trait("Bug", "748")] public void Check_for_placeholder_at_end_of_template() { // Arrange, Act _result = _finder.Find("/upstream/test/", Empty, "/upstream/test/{testId}"); // Assert ThenSinglePlaceholderIs("{testId}", Empty); } [Theory] [Trait("Bug", "748")] [InlineData("/api/invoices/{url}", "/api/invoices/123", "{url}", "123")] [InlineData("/api/invoices/{url}", "/api/invoices/", "{url}", "")] [InlineData("/api/invoices/{url}", "/api/invoices", "{url}", "")] [InlineData("/api/{version}/invoices/", "/api/v1/invoices/", "{version}", "v1")] public void Should_fix_issue_748(string template, string path, string placeholderName, string placeholderValue) { // Arrange, Act _result = _finder.Find(path, Empty, template); // Assert ThenSinglePlaceholderIs(placeholderName, placeholderValue); } [Theory] [Trait("Bug", "748")] [InlineData("/api/{version}/invoices/{url}", "/api/v1/invoices/123", "{version}", "v1", "{url}", "123")] [InlineData("/api/{version}/invoices/{url}", "/api/v1/invoices/", "{version}", "v1", "{url}", "")] [InlineData("/api/invoices/{url}?{query}", "/api/invoices/test?query=1", "{url}", "test", "{query}", "query=1")] [InlineData("/api/invoices/{url}?{query}", "/api/invoices/?query=1", "{url}", "", "{query}", "query=1")] public void Should_resolve_catchall_at_end_with_middle_placeholder(string template, string path, string placeholderName, string placeholderValue, string catchallName, string catchallValue) { // Arrange, Act _result = _finder.Find(path, Empty, template); // Assert ThenTheTemplatesVariablesAre( new(placeholderName, placeholderValue), new(catchallName, catchallValue)); } [Theory] [Trait("Bug", "2199")] [InlineData("/api/invoices/{url}-abcd", "/api/invoices/123-abcd", "{url}", "123")] [InlineData("/api/invoices/{url1}-{url2}_abcd", "/api/invoices/123-456_abcd", "{url1}", "123")] public void Can_match_between_slashes(string template, string path, string placeholderName, string placeholderValue) { // Arrange, Act _result = _finder.Find(path, Empty, template); // Assert ThenSinglePlaceholderIs(placeholderName, placeholderValue); } [Theory] [Trait("Bug", "2199")] [Trait("Feat", "2200")] [InlineData("/api/invoices_{url0}/{url1}-{url2}_abcd/{url3}?urlId={url4}", "/api/invoices_super/123-456_abcd/789?urlId=987", "{url0}", "super", "{url1}", "123", "{url2}", "456", "{url3}", "789", "{url4}", "987")] [InlineData("/api/users/{userId}/posts/{postId}_abcd/{timestamp}?filter={filter}", "/api/users/101/posts/2022_abcd/2024?filter=active", "{userId}", "101", "{postId}", "2022", "{timestamp}", "2024", "{filter}", "active")] [InlineData("/api/categories/{categoryId}/{subCategoryId}_abcd/{itemId}?sort={sortBy}", "/api/categories/1/2_abcd/5?sort=desc", "{categoryId}", "1", "{subCategoryId}", "2", "{itemId}", "5", "{sortBy}", "desc")] [InlineData("/api/products/{productId}/{category}_{itemId}_details/{status}", "/api/products/789/electronics_123_details/available", "{productId}", "789", "{category}", "electronics", "{itemId}", "123", "{status}", "available")] public void Can_match_all_placeholders_between_slashes(string template, string path, string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2, string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4, string placeholderName5 = null, string placeholderValue5 = null) { // Arrange var expectedTemplates = new List { new(placeholderName1, placeholderValue1), new(placeholderName2, placeholderValue2), new(placeholderName3, placeholderValue3), new(placeholderName4, placeholderValue4), }; // Add optional placeholders if they exist if (!string.IsNullOrEmpty(placeholderName5)) { expectedTemplates.Add(new(placeholderName5, placeholderValue5)); } // Act _result = _finder.Find(path, Empty, template); // Assert ThenTheTemplatesVariablesAre(expectedTemplates.ToArray()); } [Theory] [Trait("Bug", "2209")] [InlineData( "/entities/{id}/events/recordsdata/{subCategoryId}_abcd/{itemId}?sort={sortBy}", "/Entities/43/Events/RecordsData/2_abcd/5?sort=desc", "{id}", "43", "{subCategoryId}", "2", "{itemId}", "5", "{sortBy}", "desc")] [InlineData( "/api/PRODUCTS/{productId}/{category}_{itemId}_DeTails/{status}", "/API/Products/789/electronics_123_details/available", "{productId}", "789", "{category}", "electronics", "{itemId}", "123", "{status}", "available")] public void Find_CaseInsensitive_MatchedAllPlaceholdersBetweenSlashes(string template, string path, string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2, string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4) { // Arrange var expectedTemplates = new List { new(placeholderName1, placeholderValue1), new(placeholderName2, placeholderValue2), new(placeholderName3, placeholderValue3), new(placeholderName4, placeholderValue4), }; // Act _result = _finder.Find(path, Empty, template); // Assert ThenTheTemplatesVariablesAre(expectedTemplates.ToArray()); } [Theory] [Trait("Bug", "2209")] [InlineData( "/entities/{Id}/events/recordsdata/{subCategoryId}_abcd/{itemId}?sort={sortBy}", "/Entities/43/Events/RecordsData/2_abcd/5?sort=desc", "{id}", "43", "{subcategoryid}", "2", "{itemid}", "5", "{sortby}", "desc")] [InlineData( "/api/PRODUCTS/{productid}/{category}_{itemid}_DeTails/{status}", "/API/Products/789/electronics_123_details/available", "{productId}", "789", "{Category}", "electronics", "{itemId}", "123", "{Status}", "available")] public void Find_CaseInsensitive_CannotMatchPlaceholders(string template, string path, string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2, string placeholderName3, string placeholderValue3, string placeholderName4, string placeholderValue4) { // Arrange var expectedTemplates = new List { new(placeholderName1, placeholderValue1), new(placeholderName2, placeholderValue2), new(placeholderName3, placeholderValue3), new(placeholderName4, placeholderValue4), }; // Act _result = _finder.Find(path, Empty, template); // Assert; ThenTheExpectedVariablesCantBeFound(expectedTemplates.ToArray()); } [Theory] [Trait("Bug", "2212")] [InlineData("/dati-registri/{version}/{everything}", "/dati-registri/v1.0/operatore/R80QQ5J9600/valida", "{version}", "v1.0", "{everything}", "operatore/R80QQ5J9600/valida")] [InlineData("/api/invoices/{invoiceId}/{url}", "/api/invoices/1", "{invoiceId}", "1", "{url}", "")] [InlineData("/api/{version}/{type}/{everything}", "/api/v1.0/items/details/12345", "{version}", "v1.0", "{type}", "items", "{everything}", "details/12345")] [InlineData("/resources/{area}/{id}/{details}", "/resources/europe/56789/info/about", "{area}", "europe", "{id}", "56789", "{details}", "info/about")] [InlineData("/data/{version}/{category}/{subcategory}/{rest}", "/data/2.1/sales/reports/weekly/summary", "{version}", "2.1", "{category}", "sales", "{subcategory}", "reports", "{rest}", "weekly/summary")] [InlineData("/users/{region}/{team}/{userId}/{details}", "/users/north/eu/12345/activities/list", "{region}", "north", "{team}", "eu", "{userId}", "12345", "{details}", "activities/list")] public void Find_HasCatchAll_OnlyTheLastPlaceholderCanContainSlashes(string template, string path, string placeholderName1, string placeholderValue1, string placeholderName2, string placeholderValue2, string placeholderName3 = null, string placeholderValue3 = null, string placeholderName4 = null, string placeholderValue4 = null) { // Arrange var expectedTemplates = new List { new(placeholderName1, placeholderValue1), new(placeholderName2, placeholderValue2), }; if (!string.IsNullOrEmpty(placeholderName3)) { expectedTemplates.Add(new(placeholderName3, placeholderValue3)); } if (!string.IsNullOrEmpty(placeholderName4)) { expectedTemplates.Add(new(placeholderName4, placeholderValue4)); } // Act _result = _finder.Find(path, Empty, template); // Assert ThenTheTemplatesVariablesAre(expectedTemplates.ToArray()); } private void ThenSinglePlaceholderIs(string expectedName, string expectedValue) { var item = _result.Data.Single(t => t.Name == expectedName); item.Value.ShouldBe(expectedValue); } private void ThenTheTemplatesVariablesAre(params PlaceholderNameAndValue[] collection) { foreach (var expected in collection) { ThenSinglePlaceholderIs(expected.Name, expected.Value); } } private void ThenTheExpectedVariablesCantBeFound(params PlaceholderNameAndValue[] collection) { foreach (var expected in collection) { _result.Data.FirstOrDefault(t => t.Name == expected.Name).ShouldBeNull(); } } } ================================================ FILE: test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator; namespace Ocelot.UnitTests.DownstreamUrlCreator; public class DownstreamPathPlaceholderReplacerTests : UnitTest { private readonly DownstreamPathPlaceholderReplacer _replacer = new(); [Fact] public void Can_replace_no_template_variables() { // Arrange var holder = new DownstreamRouteHolder( new List(), GivenRoute()); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe(string.Empty); } [Fact] public void Can_replace_no_template_variables_with_slash() { // Arrange var holder = new DownstreamRouteHolder( new List(), GivenRoute("/")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("/"); } [Fact] public void Can_replace_url_no_slash() { // Arrange var holder = new DownstreamRouteHolder( new List(), GivenRoute("api")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("api"); } [Fact] public void Can_replace_url_one_slash() { // Arrange var holder = new DownstreamRouteHolder( new List(), GivenRoute("api/")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("api/"); } [Fact] public void Can_replace_url_multiple_slash() { // Arrange var holder = new DownstreamRouteHolder( new List(), GivenRoute("api/product/products/")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("api/product/products/"); } [Fact] public void Can_replace_url_one_template_variable() { // Arrange var templateVariables = new List { new("{productId}", "1"), }; var holder = new DownstreamRouteHolder( templateVariables, GivenRoute("productservice/products/{productId}/")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("productservice/products/1/"); } [Fact] public void Can_replace_url_one_template_variable_with_path_after() { // Arrange var templateVariables = new List { new("{productId}", "1"), }; var holder = new DownstreamRouteHolder( templateVariables, GivenRoute("productservice/products/{productId}/variants")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("productservice/products/1/variants"); } [Fact] public void Can_replace_url_two_template_variable() { // Arrange var templateVariables = new List { new("{productId}", "1"), new("{variantId}", "12"), }; var holder = new DownstreamRouteHolder( templateVariables, GivenRoute("productservice/products/{productId}/variants/{variantId}")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("productservice/products/1/variants/12"); } [Fact] public void Can_replace_url_three_template_variable() { // Arrange var templateVariables = new List { new("{productId}", "1"), new("{variantId}", "12"), new("{categoryId}", "34"), }; var holder = new DownstreamRouteHolder( templateVariables, GivenRoute("productservice/category/{categoryId}/products/{productId}/variants/{variantId}")); // Act var dsPath = _replacer.Replace(holder.Route.DownstreamRoute[0].DownstreamPathTemplate.Value, holder.TemplatePlaceholderNameAndValues); // Assert dsPath.Value.ShouldBe("productservice/category/34/products/1/variants/12"); } private static Route GivenRoute(string downstream = null, string method = null) { var route = GivenDownstreamRoute(downstream, method); return new() { DownstreamRoute = [route], UpstreamHttpMethod = [method is null ? HttpMethod.Get : new(method)], }; } private static DownstreamRoute GivenDownstreamRoute(string downstream = null, string method = null) => new DownstreamRouteBuilder() .WithDownstreamPathTemplate(downstream) .WithUpstreamHttpMethod([method ?? HttpMethods.Get]) .Build(); } ================================================ FILE: test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.DownstreamUrlCreator; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Values; namespace Ocelot.UnitTests.DownstreamUrlCreator; public sealed class DownstreamUrlCreatorMiddlewareTests : UnitTest { // TODO: Convert to integration tests to use real IDownstreamPathPlaceholderReplacer service (no mocking). There are a lot of failings // private readonly IDownstreamPathPlaceholderReplacer _replacer; private readonly Mock _replacer; private DownstreamPath _downstreamPath; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly DownstreamUrlCreatorMiddleware _middleware; private readonly RequestDelegate _next; private readonly HttpRequestMessage _request; private readonly DefaultHttpContext _httpContext; public DownstreamUrlCreatorMiddlewareTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _replacer = new Mock(); _request = new HttpRequestMessage(HttpMethod.Get, "https://my.url/abc/?q=123"); _next = context => Task.CompletedTask; _middleware = new DownstreamUrlCreatorMiddleware(_next, _loggerFactory.Object, _replacer.Object); } [Fact] public async Task Should_replace_scheme_and_path() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List(), new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("/api/products/1"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); ThenTheQueryStringIs("?q=123"); } [Fact] public async Task ShouldThrowNotSupportedException_WhenReplacerReturnedEmpty() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/" + TestName()) .WithUpstreamHttpMethod(["Get"]) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List(), new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("/api/products/1"); _downstreamPath = new DownstreamPath(string.Empty); _replacer .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) .Returns(_downstreamPath); // Act, Assert var ex = await Assert.ThrowsAsync(() => _middleware.Invoke(_httpContext)); Assert.Equal("IDownstreamPathPlaceholderReplacerProxy returned an empty DownstreamPath for the route /ShouldThrowNotSupportedException_WhenReplacerReturnedEmpty.", ex.Message); } [Fact] [Trait("Feat", "467")] public async Task Should_replace_query_string() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{subscriptionId}", "1"), new("{unitId}", "2"), }, new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates"); ThenTheQueryStringIs(string.Empty); } [Fact] [Trait("Feat", "467")] public async Task Should_replace_query_string_but_leave_non_placeholder_queries() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{subscriptionId}", "1"), new("{unitId}", "2"), }, new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2&productId=2"); // unitId is the first GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); ThenTheQueryStringIs("?productId=2"); } [Fact] [Trait("Bug", "1288")] public async Task Should_replace_query_string_but_leave_non_placeholder_queries_2() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{subscriptionId}", "1"), new("{unitId}", "2"), }, new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?productId=2&unitId=2"); // unitId is the second GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates?productId=2"); ThenTheQueryStringIs("?productId=2"); } [Fact] [Trait("Feat", "467")] public async Task Should_replace_query_string_exact_match() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{subscriptionId}", "1"), new("{unitId}", "2"), new("{unitIdIty}", "3"), }, new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://localhost:5000/api/subscriptions/1/updates?unitId=2?unitIdIty=3"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("api/units/1/2/updates/3"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://localhost:5000/api/units/1/2/updates/3"); ThenTheQueryStringIs(string.Empty); } [Fact] public async Task Should_not_create_service_fabric_url() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithUpstreamHttpMethod(new List { "Get" }) .WithDownstreamScheme("https") .Build(); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List(), new Route(downstreamRoute, HttpMethod.Get))); GivenTheDownstreamRequestUriIs("http://my.url/abc?q=123"); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn("/api/products/1"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://my.url:80/api/products/1?q=123"); } [Fact] public async Task Should_create_service_fabric_url() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(downstreamRouteHolder); GivenTheServiceProviderConfigIs(config); GivenTheDownstreamRequestUriIs("http://localhost:19081"); GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1"); } [Fact] public async Task Should_create_service_fabric_url_with_query_string_for_stateless_service() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(downstreamRouteHolder); GivenTheServiceProviderConfigIs(config); GivenTheDownstreamRequestUriIs("http://localhost:19081?Tom=test&laura=1"); GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?Tom=test&laura=1"); } [Fact] public async Task Should_create_service_fabric_url_with_query_string_for_stateful_service() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithServiceName("Ocelot/OcelotApp") .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(downstreamRouteHolder); GivenTheServiceProviderConfigIs(config); GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); } [Fact] public async Task Should_create_service_fabric_url_with_version_from_upstream_path_template() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamScheme("http") .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/products").Build()) .WithServiceName("Service_1.0/Api") .Build(); var routeHolder = new DownstreamRouteHolder( new List(), new Route(route)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(routeHolder); GivenTheServiceProviderConfigIs(config); GivenTheDownstreamRequestUriIs("http://localhost:19081?PartitionKind=test&PartitionKey=1"); GivenTheUrlReplacerWillReturnSequence("/products", "Service_1.0/Api"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:19081/Service_1.0/Api/products?PartitionKind=test&PartitionKey=1"); } [Fact] [Trait("Bug", "473")] public async Task Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() { // Arrange var methods = new List { "Post", "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{action}", "1"), new("{servak}", "2"), }, new Route(downstreamRoute))); GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn("/Authorized/1?server=2"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?server=2&refreshToken=123456789"); ThenTheQueryStringIs("?server=2&refreshToken=123456789"); } [Fact] public async Task Should_not_replace_by_empty_scheme() { // Arrange var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme(string.Empty) .WithServiceName("Ocelot/OcelotApp") .Build(); var downstreamRouteHolder = new DownstreamRouteHolder( new List(), new Route(downstreamRoute)); var config = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .WithHost("localhost") .WithPort(19081) .Build(); GivenTheDownStreamRouteIs(downstreamRouteHolder); GivenTheServiceProviderConfigIs(config); GivenTheDownstreamRequestUriIs("https://localhost:19081?PartitionKind=test&PartitionKey=1"); GivenTheUrlReplacerWillReturnSequence("/api/products/1", "Ocelot/OcelotApp"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("https://localhost:19081/Ocelot/OcelotApp/api/products/1?PartitionKind=test&PartitionKey=1"); } [Fact] [Trait("Bug", "952")] public async Task Should_map_query_parameters_with_different_names() { // Arrange var methods = new List { "Post", "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/users?userId={userId}").Build()) .WithDownstreamPathTemplate("/persons?personId={userId}") .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{userId}", "webley"), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn("/persons?personId=webley"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley"); ThenTheQueryStringIs($"?personId=webley"); } [Fact] [Trait("Bug", "952")] public async Task Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() { // Arrange var methods = new List { "Post", "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/users?userId={uid}").Build()) .WithDownstreamPathTemplate("/persons?personId={uid}") .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{uid}", "webley"), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn("/persons?personId=webley"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley"); ThenTheQueryStringIs($"?personId=webley&userId=webley"); } [Theory] [Trait("Bug", "1174")] // https://github.com/ThreeMammals/Ocelot/issues/1174 [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "projectNumber=45&startDate=2019-12-12&endDate=2019-12-12")] [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00%3A00%3A00z%20and%20DateOfSale%20le%202020-03-15T00%3A00%3A00z")] public async Task Should_forward_query_parameters_without_duplicates(string everythingelse, string query) { // Arrange var methods = new List { "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/contracts?{everythingelse}").Build()) .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{everythingelse}", everythingelse), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{query}"); ThenTheQueryStringIs($"?{query}"); } [Theory] [Trait("Bug", "748")] // https://github.com/ThreeMammals/Ocelot/issues/748 [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123", "{url}", "123", "/api/v1/test/123", "")] [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/123?query=1", "{url}", "123", "/api/v1/test/123?query=1", "?query=1")] [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/?query=1", "{url}", "", "/api/v1/test/?query=1", "?query=1")] [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1?query=1", "{url}", "", "/api/v1/test?query=1", "?query=1")] [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1/", "{url}", "", "/api/v1/test/", "")] [InlineData("/test/{version}/{url}", "/api/{version}/test/{url}", "/test/v1", "{url}", "", "/api/v1/test", "")] public async Task Should_fix_issue_748(string upstreamTemplate, string downstreamTemplate, string requestURL, string placeholderName, string placeholderValue, string downstreamURI, string queryString) { // Arrange var methods = new List { "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue(upstreamTemplate).Build()) .WithDownstreamPathTemplate(downstreamTemplate) .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new(placeholderName, placeholderValue), new("{version}", "v1"), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs("http://localhost:5000" + requestURL); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn(downstreamURI); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:5000" + downstreamURI); ThenTheQueryStringIs(queryString); } [Fact] [Trait("Bug", "748")] // https://github.com/ThreeMammals/Ocelot/issues/748 public async Task Should_omit_the_ending_slash_from_the_downstream_path_when_the_upstream_path_has_no_ending_slash() { // Arrange var methods = new List { "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/test/{version}/{url}").Build()) .WithDownstreamPathTemplate("/api/{version}/test/{url}/") // !!! ending slash .WithUpstreamHttpMethod(methods) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{url}", "abcd"), new("{version}", "v1"), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(methods) } )); GivenTheDownstreamRequestUriIs("http://localhost:5000" + "/test/v1/abcd"); // upstream has no ending slash GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn("/api/v1/test/abcd/"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs("http://localhost:5000" + "/api/v1/test/abcd"); ThenTheQueryStringIs(""); } [Fact] [Trait("Bug", "2002")] public async Task Should_map_when_query_parameters_has_same_names_with_placeholder() { // Arrange const string username = "bbenameur"; const string groupName = "Paris"; const string roleid = "123456"; const string everything = "something=9874565"; var withGetMethod = new List { "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}") .Build()) .WithDownstreamPathTemplate("/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}") .WithUpstreamHttpMethod(withGetMethod) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{username}", username), new("{groupName}", groupName), new("{roleid}", roleid), new("{everything}", everything), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/WeatherForecast/{roleid}/groups?username={username}&groupName={groupName}&{everything}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn($"/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs($"http://localhost:5000/account/{username}/groups/{groupName}/roles?roleId={roleid}&{everything}"); ThenTheQueryStringIs($"?roleId={roleid}&{everything}"); } [Theory] [Trait("Bug", "2116")] [InlineData("api/debug()")] // no query [InlineData("api/debug%28%29")] // debug() public async Task ShouldNotFailToHandleUrlWithSpecialRegexChars(string urlPath) { // Arrange var withGetMethod = new List { "Get" }; var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() .WithOriginalValue("/routed/api/{path}") .Build()) .WithDownstreamPathTemplate("/api/{path}") .WithUpstreamHttpMethod(withGetMethod) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( new List { new("{path}", urlPath), }, new Route(downstreamRoute) { UpstreamHttpMethod = AsHashSet(withGetMethod) } )); GivenTheDownstreamRequestUriIs($"http://localhost:5000/{urlPath}"); GivenTheServiceProviderConfigIs(new ServiceProviderConfigurationBuilder().Build()); GivenTheUrlReplacerWillReturn($"routed/{urlPath}"); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs($"http://localhost:5000/routed/{urlPath}"); Assert.Equal((int)HttpStatusCode.OK, _httpContext.Response.StatusCode); } [Theory] [Trait("PR", "2351")] // https://github.com/ThreeMammals/Ocelot/pull/2351 [Trait("Bug", "2346")] // https://github.com/ThreeMammals/Ocelot/issues/2346 [InlineData("/v1/payment-methods", "{id}", "http://localhost:5003/v1/payment-methods?customer_id=12345", null, "?customer_id=12345")] [InlineData("/v1/orders", "{id}", "http://localhost:5003/v1/orders?orderid=999&customer_id=12345", null, "?orderid=999&customer_id=12345")] [InlineData("/v1/users", "{customer_id}", "http://localhost:5003/v1/users?id=999", null, "?id=999")] [InlineData("/v1/records", "{id1}", "http://localhost:5003/v1/records?id1=123&id10=456", "http://localhost:5003/v1/records?id10=456", "?id10=456")] public async Task Should_not_corrupt_query_parameter_names_containing_id_when_route_has_id_placeholder( string downstreamPath, string placeholder, string url, string downstreamUrl, string query) { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate(downstreamPath) .WithUpstreamHttpMethod([HttpMethods.Get]) .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); var config = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamRouteIs(new DownstreamRouteHolder( [ new(placeholder, "123") ], new(downstreamRoute, HttpMethod.Get) )); GivenTheDownstreamRequestUriIs(url); GivenTheServiceProviderConfigIs(config); GivenTheUrlReplacerWillReturn(downstreamPath); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheDownstreamRequestUriIs(downstreamUrl ?? url); ThenTheQueryStringIs(query); } private static ReadOnlySpan GetPath(string downstreamPath) => DownstreamUrlCreatorMiddlewareTestWrapper.GetPath(downstreamPath); [Theory] [InlineData("/api/resource?param=value", "/api/resource")] [InlineData("/api/resource?x=1&y=2", "/api/resource")] public void GetPath_ShouldReturnSubstringBeforeQuestionMark(string input, string expected) { // Act var result = GetPath(input); // Assert Assert.Equal(expected, result.ToString()); } [Theory] [InlineData("/api/resource", "/api/resource")] [InlineData("/api/resource/123", "/api/resource/123")] public void GetPath_ShouldReturnFullPath_WhenNoQuestionMark(string input, string expected) { // Act var result = GetPath(input); // Assert Assert.Equal(expected, result.ToString()); } [Fact] public void GetPath_ShouldHandleEmptyString() { // Act var result = GetPath(string.Empty); // Assert Assert.Equal(string.Empty, result.ToString()); } private static ReadOnlySpan GetQueryString(string downstreamPath) => DownstreamUrlCreatorMiddlewareTestWrapper.GetQueryString(downstreamPath); [Theory] [InlineData("/api/resource?param=value", "?param=value")] [InlineData("/api/resource?x=1&y=2", "?x=1&y=2")] public void GetQueryString_ShouldReturnSubstringStartingWithQuestionMark(string input, string expected) { // Act var result = GetQueryString(input); // Assert Assert.Equal(expected, result.ToString()); } [Theory] [InlineData("/api/resource", "")] [InlineData("/api/resource/123", "")] public void GetQueryString_ShouldReturnEmpty_WhenNoQuestionMark(string input, string expected) { // Act var result = GetQueryString(input); // Assert Assert.Equal(expected, result.ToString()); } [Fact] public void GetQueryString_ShouldHandleEmptyString() { // Act var result = GetQueryString(string.Empty); // Assert Assert.Equal(string.Empty, result.ToString()); } [Fact] public void GetQueryString_ShouldHandleOnlyQuestionMark() { // Act var result = GetQueryString("?"); // Assert Assert.Equal("?", result.ToString()); } private static HashSet AsHashSet(IEnumerable collection) => collection.Select(AsHttpMethod).ToHashSet(); private static HttpMethod AsHttpMethod(string method) => new(method); private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { var configuration = new InternalConfiguration() { ServiceProviderConfiguration = config, }; _httpContext.Items.SetIInternalConfiguration(configuration); } private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) { _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void GivenTheDownstreamRequestUriIs(string uri) { _request.RequestUri = new Uri(uri); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_request)); } private void GivenTheUrlReplacerWillReturnSequence(params string[] paths) { var setup = _replacer .SetupSequence(x => x.Replace(It.IsAny(), It.IsAny>())); foreach (var path in paths) { setup.Returns(new DownstreamPath(path)); } } private void GivenTheUrlReplacerWillReturn(string path) { _downstreamPath = new DownstreamPath(path); _replacer .Setup(x => x.Replace(It.IsAny(), It.IsAny>())) .Returns(_downstreamPath); } private void ThenTheDownstreamRequestUriIs(string expectedUri) { _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe(expectedUri); } private void ThenTheQueryStringIs(string queryString) { _httpContext.Items.DownstreamRequest().Query.ShouldBe(queryString); } } internal class DownstreamUrlCreatorMiddlewareTestWrapper : DownstreamUrlCreatorMiddleware { public DownstreamUrlCreatorMiddlewareTestWrapper(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IDownstreamPathPlaceholderReplacer replacer) : base(next, loggerFactory, replacer) { } public static new ReadOnlySpan GetPath(ReadOnlySpan downstreamPath) => DownstreamUrlCreatorMiddleware.GetPath(downstreamPath); public static new ReadOnlySpan GetQueryString(ReadOnlySpan downstreamPath) => DownstreamUrlCreatorMiddleware.GetQueryString(downstreamPath); } ================================================ FILE: test/Ocelot.UnitTests/Errors/ErrorTests.cs ================================================ using Ocelot.Infrastructure.RequestData; namespace Ocelot.UnitTests.Errors; public class ErrorTests { [Fact] public void Should_return_message() { // Arrange var error = new CannotAddDataError("message"); // Act var result = error.ToString(); // Assert result.ShouldBe("CannotAddDataError: message"); } } ================================================ FILE: test/Ocelot.UnitTests/Errors/ExceptionHandlerMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Errors; using Ocelot.Errors.Middleware; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; namespace Ocelot.UnitTests.Errors; public class ExceptionHandlerMiddlewareTests : UnitTest { private bool _shouldThrowAnException; private readonly Mock _repo; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ExceptionHandlerMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ExceptionHandlerMiddlewareTests() { _httpContext = new DefaultHttpContext(); _repo = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = async context => { await Task.CompletedTask; if (_shouldThrowAnException) { throw new Exception("BOOM"); } _httpContext.Response.StatusCode = (int)HttpStatusCode.OK; }; _middleware = new ExceptionHandlerMiddleware(_next, _loggerFactory.Object, _repo.Object); } [Fact] public async Task NoDownstreamException() { // Arrange _shouldThrowAnException = false; var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); _repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task DownstreamException() { // Arrange _shouldThrowAnException = true; var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError); } [Fact] public async Task ShouldSetRequestId() { // Arrange _shouldThrowAnException = false; var config = new InternalConfiguration() { RequestId = "requestidkey", }; _httpContext.Items.Add(nameof(IInternalConfiguration), config); _httpContext.Request.Headers.Append("requestidkey", "1234"); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); _repo.Verify(x => x.Add("RequestId", "1234"), Times.Once); } [Fact] public async Task ShouldSetAspDotNetRequestId() { // Arrange _shouldThrowAnException = false; var config = new InternalConfiguration(); _httpContext.Items.Add(nameof(IInternalConfiguration), config); _httpContext.Request.Headers.Append("requestidkey", "1234"); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); _repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Once); } [Fact] public async Task Should_throw_exception_if_config_provider_throws() { // Arrange _shouldThrowAnException = false; // this will break when we handle not having the configuratio in the items dictionary _httpContext.Items = new Dictionary(); _httpContext.Request.Headers.Append("requestidkey", "1234"); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError); } private class FakeError : Error { internal FakeError() : base("meh", OcelotErrorCode.CannotAddDataError, 404) { } } } ================================================ FILE: test/Ocelot.UnitTests/Eureka/EurekaMiddlewareConfigurationProviderTests.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Repository; using Ocelot.Provider.Eureka; using Ocelot.Responses; using Steeltoe.Discovery; namespace Ocelot.UnitTests.Eureka; public class EurekaMiddlewareConfigurationProviderTests { [Fact] public void ShouldNotBuild() { // Arrange var configRepo = new Mock(); configRepo.Setup(x => x.Get()) .Returns(new OkResponse(new InternalConfiguration())); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); var sp = services.BuildServiceProvider(true); // Act var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); // Assert provider.Status.ShouldBe(TaskStatus.RanToCompletion); } [Fact] public void ShouldBuild() { // Arrange var serviceProviderConfig = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); var client = new Mock(); var configRepo = new Mock(); configRepo.Setup(x => x.Get()) .Returns(new OkResponse(new InternalConfiguration() { ServiceProviderConfiguration = serviceProviderConfig })); var services = new ServiceCollection(); services.AddSingleton(configRepo.Object); services.AddSingleton(client.Object); var sp = services.BuildServiceProvider(true); // Act var provider = EurekaMiddlewareConfigurationProvider.Get(new ApplicationBuilder(sp)); // Assert provider.Status.ShouldBe(TaskStatus.RanToCompletion); } } ================================================ FILE: test/Ocelot.UnitTests/Eureka/EurekaProviderFactoryTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.Builder; using Ocelot.Provider.Eureka; using Steeltoe.Discovery; namespace Ocelot.UnitTests.Eureka; public class EurekaProviderFactoryTests { [Fact] public void Should_not_get() { // Arrange var config = new ServiceProviderConfigurationBuilder().Build(); var sp = new ServiceCollection().BuildServiceProvider(true); // Act, Assert Should.Throw(() => EurekaProviderFactory.Get(sp, config, null)); } [Fact] public void Should_get() { // Arrange var config = new ServiceProviderConfigurationBuilder().WithType("eureka").Build(); var client = new Mock(); var services = new ServiceCollection(); services.AddSingleton(client.Object); var sp = services.BuildServiceProvider(true); var route = new DownstreamRouteBuilder() .WithServiceName(string.Empty) .Build(); // Act var provider = EurekaProviderFactory.Get(sp, config, route); // Assert provider.ShouldBeOfType(); } } ================================================ FILE: test/Ocelot.UnitTests/Eureka/EurekaServiceDiscoveryProviderTests.cs ================================================ using Ocelot.Values; using Steeltoe.Common.Discovery; using Steeltoe.Discovery; using _Eureka_ = Ocelot.Provider.Eureka.Eureka; namespace Ocelot.UnitTests.Eureka; public class EurekaServiceDiscoveryProviderTests : UnitTest { private readonly _Eureka_ _provider; private readonly Mock _client; private readonly string _serviceId; private List _result; public EurekaServiceDiscoveryProviderTests() { _serviceId = "Laura"; _client = new Mock(); _provider = new _Eureka_(_serviceId, _client.Object); } [Fact] public async Task Should_return_empty_services() { // Arrange, Act _result = await _provider.GetAsync(); // Assert _result.Count.ShouldBe(0); } [Fact] public async Task Should_return_service_from_client() { // Arrange var instances = new List { new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), }; _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); // Act _result = await _provider.GetAsync(); _result.Count.ShouldBe(1); // Assert _client.Verify(x => x.GetInstances(_serviceId), Times.Once); // Assert: Then The Service Is Mapped _result[0].HostAndPort.DownstreamHost.ShouldBe("somehost"); _result[0].HostAndPort.DownstreamPort.ShouldBe(801); _result[0].Name.ShouldBe(_serviceId); } [Fact] public async Task Should_return_services_from_client() { // Arrange var instances = new List { new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), new EurekaService(_serviceId, "somehost", 801, false, new Uri("http://somehost:801"), new Dictionary()), }; _client.Setup(x => x.GetInstances(It.IsAny())).Returns(instances); // Act _result = await _provider.GetAsync(); // Assert _result.Count.ShouldBe(2); _client.Verify(x => x.GetInstances(_serviceId), Times.Once); } } public class EurekaService : IServiceInstance { public EurekaService(string serviceId, string host, int port, bool isSecure, Uri uri, IDictionary metadata) { ServiceId = serviceId; Host = host; Port = port; IsSecure = isSecure; Uri = uri; Metadata = metadata; } public string ServiceId { get; } public string Host { get; } public int Port { get; } public bool IsSecure { get; } public Uri Uri { get; } public IDictionary Metadata { get; } } ================================================ FILE: test/Ocelot.UnitTests/Eureka/OcelotBuilderExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Ocelot.Provider.Eureka; using Ocelot.ServiceDiscovery; using Steeltoe.Common.Discovery; using Steeltoe.Common.Http.Discovery; using System.Reflection; namespace Ocelot.UnitTests.Eureka; public sealed class OcelotBuilderExtensionsTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment.Setup(e => e.ApplicationName) .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); return environment.Object; } [Fact] [Trait("PR", "734")] [Trait("Feat", "324")] [Trait("Feat", "844")] public void AddEureka_NoExceptions_ShouldSetUpEureka() { // Arrange var addOcelot = () => _ocelotBuilder = _services.AddOcelot(_configRoot); addOcelot.ShouldNotThrow(); // Act var addEureka = () => _ocelotBuilder.AddEureka(); // Assert addEureka.ShouldNotThrow(); } [Fact] [Trait("PR", "734")] [Trait("Feat", "324")] [Trait("Feat", "844")] public void AddEureka_DefaultServices_HappyPath() { // Arrange, Act _ocelotBuilder = _services.AddOcelot(_configRoot).AddEureka(); // Assert: AddDiscoveryClient var descriptor = _services.SingleOrDefault(Of).ShouldNotBeNull(); descriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); descriptor = _services.SingleOrDefault(Of).ShouldNotBeNull(); descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); // Assert descriptor = _services.SingleOrDefault(Of).ShouldNotBeNull(); descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); descriptor = _services.SingleOrDefault(Of).ShouldNotBeNull(); descriptor.Lifetime.ShouldBe(ServiceLifetime.Singleton); } private static bool Of(ServiceDescriptor descriptor) where TType : class => descriptor.ServiceType.Equals(typeof(TType)); } ================================================ FILE: test/Ocelot.UnitTests/FileUnitTest.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests; public class FileUnitTest : FileUnit { //protected static FileRouteBox Box(FileRoute route) => new(route); } ================================================ FILE: test/Ocelot.UnitTests/Headers/AddHeadersToRequestClaimToThingTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Errors; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Logging; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Headers; /// /// Feature: Claims to Headers. /// [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class AddHeadersToRequestClaimToThingTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private readonly Mock _parser; private readonly DownstreamRequest _downstreamRequest; private readonly Mock _placeholders; private readonly Mock _factory; public AddHeadersToRequestClaimToThingTests() { _parser = new Mock(); _placeholders = new Mock(); _factory = new Mock(); _addHeadersToRequest = new AddHeadersToRequest(_parser.Object, _placeholders.Object, _factory.Object); _downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com")); } [Fact] public void Should_add_headers_to_downstreamRequest() { // Arrange var claims = new List { new("test", "data"), }; var configuration = new List { new("header-key", string.Empty, string.Empty, 0), }; var claimValue = GivenTheClaimParserReturns(new OkResponse("value")); // Act var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeFalse(); ThenTheHeaderIsAdded(claimValue); } [Fact] public void Should_replace_existing_headers_on_request() { // Arrange var claims = new List { new("test", "data"), }; var configuration = new List { new("header-key", string.Empty, string.Empty, 0), }; var claimValue = GivenTheClaimParserReturns(new OkResponse("value")); _downstreamRequest.Headers.Add("header-key", "initial"); // Act var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeFalse(); ThenTheHeaderIsAdded(claimValue); } [Fact] public void Should_return_error() { // Arrange var claims = new List(); var configuration = new List { new(string.Empty, string.Empty, string.Empty, 0), }; _ = GivenTheClaimParserReturns(new ErrorResponse(new List { new AnyError() })); // Act var result = _addHeadersToRequest.SetHeadersOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeTrue(); } private Response GivenTheClaimParserReturns(Response claimValue) { _parser.Setup(x => x.GetValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(claimValue); return claimValue; } private void ThenTheHeaderIsAdded(Response claimValue) => _downstreamRequest.Headers.First(x => x.Key == "header-key").Value.First().ShouldBe(claimValue.Data); private class AnyError : Error { public AnyError() : base("blahh", OcelotErrorCode.UnknownError, 404) { } } } ================================================ FILE: test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Configuration.Creator; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims; using Ocelot.Logging; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Headers; public class AddHeadersToRequestPlainTests : UnitTest { private readonly AddHeadersToRequest _addHeadersToRequest; private readonly DefaultHttpContext _context; private AddHeader _addedHeader; private readonly Mock _placeholders; private readonly Mock _factory; private readonly Mock _logger; public AddHeadersToRequestPlainTests() { _placeholders = new Mock(); _factory = new Mock(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _addHeadersToRequest = new AddHeadersToRequest(Mock.Of(), _placeholders.Object, _factory.Object); _context = new DefaultHttpContext(); } [Fact] [Trait("Feat", "623")] // https://github.com/ThreeMammals/Ocelot/issues/623 [Trait("PR", "632")] // https://github.com/ThreeMammals/Ocelot/pull/632 public void Should_log_error_if_cannot_find_placeholder() { // Arrange _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); // Act WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}"); // Assert _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"Unable to add header to response X-Forwarded-For: {{RemoteIdAddress}}")), Times.Once); } [Fact] [Trait("Feat", "623")] [Trait("PR", "632")] public void Should_add_placeholder_to_downstream_request() { // Arrange _placeholders.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse("replaced")); // Act WhenAddingHeader("X-Forwarded-For", "{RemoteIdAddress}"); // Assert ThenTheHeaderGetsTakenOverToTheRequestHeaders("replaced"); } [Fact] public void Should_add_plain_text_header_to_downstream_request() { // Arrange, Act WhenAddingHeader("X-Custom-Header", "PlainValue"); // Assert ThenTheHeaderGetsTakenOverToTheRequestHeaders(); } [Fact] public void Should_overwrite_existing_header_with_added_header() { // Arrange _context.Request.Headers.Append("X-Custom-Header", "This should get overwritten"); // Act WhenAddingHeader("X-Custom-Header", "PlainValue"); // Assert ThenTheHeaderGetsTakenOverToTheRequestHeaders(); } private void WhenAddingHeader(string headerKey, string headerValue) { _addedHeader = new AddHeader(headerKey, headerValue); _addHeadersToRequest.SetHeadersOnDownstreamRequest(new[] { _addedHeader }, _context); } private void ThenTheHeaderGetsTakenOverToTheRequestHeaders() { var requestHeaders = _context.Request.Headers; requestHeaders.ContainsKey(_addedHeader.Key).ShouldBeTrue($"Header {_addedHeader.Key} was expected but not there."); var value = requestHeaders[_addedHeader.Key]; value.ShouldNotBe(default, $"Value of header {_addedHeader.Key} was expected to not be null."); value.ToString().ShouldBe(_addedHeader.Value); } private void ThenTheHeaderGetsTakenOverToTheRequestHeaders(string expected) { var requestHeaders = _context.Request.Headers; var value = requestHeaders[_addedHeader.Key]; value.ToString().ShouldBe(expected); } } ================================================ FILE: test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs ================================================ using Ocelot.Configuration.Creator; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Headers; public class AddHeadersToResponseTests : UnitTest { private readonly AddHeadersToResponse _adder; private readonly Mock _placeholders; private readonly DownstreamResponse _response; private List _addHeaders; private readonly Mock _factory; private readonly Mock _logger; public AddHeadersToResponseTests() { _factory = new Mock(); _logger = new Mock(); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _placeholders = new Mock(); _adder = new AddHeadersToResponse(_placeholders.Object, _factory.Object); _response = new DownstreamResponse(new HttpResponseMessage()); } [Fact] public void Should_add_header() { // Arrange _addHeaders = new List { new("Laura", "Tom"), }; // Act _adder.Add(_addHeaders, _response); // Assert ThenTheHeaderIsReturned("Laura", "Tom"); } [Fact] public void Should_add_trace_id_placeholder() { // Arrange _addHeaders = new List { new("Trace-Id", "{TraceId}"), }; var traceId = "123"; GivenTheTraceIdIs(traceId); // Act _adder.Add(_addHeaders, _response); // Assert ThenTheHeaderIsReturned("Trace-Id", traceId); } [Fact] public void Should_add_trace_id_placeholder_and_normal() { // Arrange _addHeaders = new List { new("Trace-Id", "{TraceId}"), new("Tom", "Laura"), }; var traceId = "123"; GivenTheTraceIdIs(traceId); // Act _adder.Add(_addHeaders, _response); // Assert ThenTheHeaderIsReturned("Trace-Id", traceId); ThenTheHeaderIsReturned("Tom", "Laura"); } [Fact] public void Should_do_nothing_and_log_error() { // Arrange _addHeaders = new List { new("Trace-Id", "{TraceId}"), }; _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new ErrorResponse(new AnyError())); // Act _adder.Add(_addHeaders, _response); // Assert _response.Headers.Any(x => x.Key == "Trace-Id").ShouldBeFalse(); _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == "Unable to add header to response Trace-Id: {TraceId}")), Times.Once); } private void GivenTheTraceIdIs(string traceId) { _placeholders.Setup(x => x.Get("{TraceId}")).Returns(new OkResponse(traceId)); } private void ThenTheHeaderIsReturned(string key, string value) { var values = _response.Headers.First(x => x.Key == key); values.Values.First().ShouldBe(value); } } ================================================ FILE: test/Ocelot.UnitTests/Headers/ClaimsToHeadersMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.Headers; using Ocelot.Headers.Middleware; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Headers; /// /// Feature: Claims to Headers. /// [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class ClaimsToHeadersMiddlewareTests : UnitTest { private readonly Mock _addHeaders; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ClaimsToHeadersMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ClaimsToHeadersMiddlewareTests() { _httpContext = new DefaultHttpContext(); _addHeaders = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new ClaimsToHeadersMiddleware(_next, _loggerFactory.Object, _addHeaders.Object); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); } [Fact] public async Task Should_call_add_headers_to_request_correctly() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToHeaders(new List { new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(["Get"]) .Build(); var downstreamRoute = new DownstreamRouteHolder( new(), new Route(route, HttpMethod.Get)); // Arrange: Given The Down Stream Route Is _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); // Arrange: Given The AddHeaders To Downstream Request Returns Ok _addHeaders.Setup(x => x.SetHeadersOnDownstreamRequest(It.IsAny>(), It.IsAny>(), It.IsAny())) .Returns(new OkResponse()); // Act await _middleware.Invoke(_httpContext); // Assert: Then The AddHeaders ToRequest Is Called Correctly _addHeaders.Verify( x => x.SetHeadersOnDownstreamRequest(It.IsAny>(), It.IsAny>(), _httpContext.Items.DownstreamRequest()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Headers/HttpContextRequestHeaderReplacerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Headers; using Ocelot.Responses; namespace Ocelot.UnitTests.Headers; public class HttpContextRequestHeaderReplacerTests : UnitTest { private readonly DefaultHttpContext _context; private readonly HttpContextRequestHeaderReplacer _replacer; public HttpContextRequestHeaderReplacerTests() { _replacer = new(); _context = new(); } [Fact] public void Should_replace_headers() { // Arrange _context.Request.Headers.Append("test", "test"); var fAndRs = new List { new("test", "test", "chiken", 0) }; // Act var result = _replacer.Replace(_context, fAndRs); // Assert result.ShouldBeOfType(); foreach (var f in fAndRs) { _context.Request.Headers.TryGetValue(f.Key, out var values); values[f.Index].ShouldBe(f.Replace); } } [Fact] public void Should_not_replace_headers() { // Arrange _context.Request.Headers.Append("test", "test"); var fAndRs = new List(); // Act var result = _replacer.Replace(_context, fAndRs); // Assert result.ShouldBeOfType(); foreach (var f in fAndRs) { _context.Request.Headers.TryGetValue(f.Key, out var values); values[f.Index].ShouldBe("test"); } } } ================================================ FILE: test/Ocelot.UnitTests/Headers/HttpHeadersTransformationMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Authorization; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Headers; using Ocelot.Headers.Middleware; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; namespace Ocelot.UnitTests.Headers; public class HttpHeadersTransformationMiddlewareTests : UnitTest { private readonly Mock _preReplacer; private readonly Mock _postReplacer; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly HttpHeadersTransformationMiddleware _middleware; private readonly RequestDelegate _next; private readonly Mock _addHeadersToResponse; private readonly Mock _addHeadersToRequest; private readonly DefaultHttpContext _httpContext; public HttpHeadersTransformationMiddlewareTests() { _httpContext = new DefaultHttpContext(); _preReplacer = new Mock(); _postReplacer = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _addHeadersToResponse = new Mock(); _addHeadersToRequest = new Mock(); _middleware = new HttpHeadersTransformationMiddleware( _next, _loggerFactory.Object, _preReplacer.Object, _postReplacer.Object, _addHeadersToResponse.Object, _addHeadersToRequest.Object); } [Fact] public async Task Should_call_pre_and_post_header_transforms() { // Arrange GivenTheFollowingRequest(); GivenTheDownstreamRequestIs(); GivenTheRouteHasPreFindAndReplaceSetUp(); GivenTheHttpResponseMessageIs(); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly(); ThenAddHeadersToRequestIsCalledCorrectly(); ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly(); ThenAddHeadersToResponseIsCalledCorrectly(); } private void ThenAddHeadersToResponseIsCalledCorrectly() { _addHeadersToResponse .Verify(x => x.Add(_httpContext.Items.DownstreamRoute().AddHeadersToDownstream, _httpContext.Items.DownstreamResponse()), Times.Once); } private void ThenAddHeadersToRequestIsCalledCorrectly() { _addHeadersToRequest .Verify(x => x.SetHeadersOnDownstreamRequest(_httpContext.Items.DownstreamRoute().AddHeadersToUpstream, _httpContext), Times.Once); } private void GivenTheDownstreamRequestIs() { _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); } private void GivenTheHttpResponseMessageIs() { _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage())); } private void GivenTheRouteHasPreFindAndReplaceSetUp() { var fAndRs = new List(); var dRoute = new DownstreamRouteBuilder() .WithUpstreamHeaderFindAndReplace(fAndRs) .WithDownstreamHeaderFindAndReplace(fAndRs) .Build(); var route = new Route(dRoute); var dR = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(null, route); _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(dR.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(dR.Route.DownstreamRoute[0]); } private void ThenTheIHttpContextRequestHeaderReplacerIsCalledCorrectly() { _preReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); } private void ThenTheIHttpResponseHeaderReplacerIsCalledCorrectly() { _postReplacer.Verify(x => x.Replace(It.IsAny(), It.IsAny>()), Times.Once); } private void GivenTheFollowingRequest() { _httpContext.Request.Headers.Append("test", "test"); } } ================================================ FILE: test/Ocelot.UnitTests/Headers/HttpResponseHeaderReplacerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.UnitTests.Headers; public class HttpResponseHeaderReplacerTests : UnitTest { private DownstreamResponse _response; private readonly Placeholders _placeholders; private readonly HttpResponseHeaderReplacer _replacer; private List _headerFindAndReplaces; private Response _result; private DownstreamRequest _request; private readonly Mock _finder; private readonly Mock _repo; private readonly Mock _accessor; /*private readonly Mock _loggerFactory; private readonly Mock _logger;*/ public HttpResponseHeaderReplacerTests() { _repo = new Mock(); _finder = new Mock(); _accessor = new Mock(); //_loggerFactory = new Mock(); _placeholders = new Placeholders(_finder.Object, _repo.Object, _accessor.Object/*,_loggerFactory.Object*/); _replacer = new HttpResponseHeaderReplacer(_placeholders); } [Fact] public void Should_replace_headers() { // Arrange _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("test", new List {"test"}), }, string.Empty); _headerFindAndReplaces = new List { new("test", "test", "chiken", 0) }; // Act WhenICallTheReplacer(); // Assert ThenTheHeadersAreReplaced(); } [Fact] public void Should_not_replace_headers() { // Arrange _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("test", new List {"test"}), }, string.Empty); _headerFindAndReplaces = new List(); // Act WhenICallTheReplacer(); // Assert ThenTheHeadersAreNotReplaced(); } [Fact] public void Should_replace_downstream_base_url_with_ocelot_base_url() { // Arrange const string downstreamUrl = "http://downstream.com/"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com/"); } [Fact] public void Should_replace_downstream_base_url_with_ocelot_base_url_with_port() { // Arrange const string downstreamUrl = "http://downstream.com/"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com:123/"); } [Fact] public void Should_replace_downstream_base_url_with_ocelot_base_url_and_path() { // Arrange const string downstreamUrl = "http://downstream.com/test/product"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com/test/product"); } [Fact] public void Should_replace_downstream_base_url_with_ocelot_base_url_with_path_and_port() { // Arrange const string downstreamUrl = "http://downstream.com/test/product"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:123/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com:123/test/product"); } [Fact] public void Should_replace_downstream_base_url_and_port_with_ocelot_base_url() { // Arrange const string downstreamUrl = "http://downstream.com:123/test/product"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com/test/product"); } [Fact] public void Should_replace_downstream_base_url_and_port_with_ocelot_base_url_and_port() { // Arrange const string downstreamUrl = "http://downstream.com:123/test/product"; _request = new DownstreamRequest(new(HttpMethod.Get, "http://test.com") { RequestUri = new Uri(downstreamUrl) }); _response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.Accepted, new List>> { new("Location", new List {downstreamUrl}), }, string.Empty); _headerFindAndReplaces = new List { new("Location", "{DownstreamBaseUrl}", "http://ocelot.com:321/", 0), }; // Act WhenICallTheReplacer(); // Assert ThenTheHeaderShouldBe("Location", "http://ocelot.com:321/test/product"); } private void ThenTheHeadersAreNotReplaced() { _result.ShouldBeOfType(); foreach (var f in _headerFindAndReplaces) { var values = _response.Headers.First(x => x.Key == f.Key); values.Values.ToList()[f.Index].ShouldBe("test"); } } private void WhenICallTheReplacer() { var httpContext = new DefaultHttpContext(); httpContext.Items.UpsertDownstreamResponse(_response); httpContext.Items.UpsertDownstreamRequest(_request); _result = _replacer.Replace(httpContext, _headerFindAndReplaces); } private void ThenTheHeaderShouldBe(string key, string value) { var test = _response.Headers.First(x => x.Key == key); test.Values.First().ShouldBe(value); } private void ThenTheHeadersAreReplaced() { _result.ShouldBeOfType(); foreach (var f in _headerFindAndReplaces) { var values = _response.Headers.First(x => x.Key == f.Key); values.Values.ToList()[f.Index].ShouldBe(f.Replace); } } } ================================================ FILE: test/Ocelot.UnitTests/Headers/RemoveHeadersTests.cs ================================================ using Ocelot.Middleware; using Ocelot.Headers; namespace Ocelot.UnitTests.Headers; public class RemoveHeadersTests : UnitTest { private readonly RemoveOutputHeaders _removeOutputHeaders = new(); [Fact] public void Should_remove_header() { // Arrange var headers = new List
{ new("Transfer-Encoding", new List {"chunked"}), }; // Act var result = _removeOutputHeaders.Remove(headers); // Assert result.IsError.ShouldBeFalse(); headers.ShouldNotContain(x => x.Key == "Transfer-Encoding"); headers.ShouldNotContain(x => x.Key == "transfer-encoding"); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/ClaimParserTests.cs ================================================ using Ocelot.Errors; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Infrastructure; /// /// Feature: Claims to Headers. /// [Trait("Commit", "84256e7")] // https://github.com/ThreeMammals/Ocelot/commit/84256e7bac0fa2c8ceba92bd8fe64c8015a37cea [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class ClaimParserTests : UnitTest { private readonly ClaimsParser _claimsParser; private readonly List _claims; public ClaimParserTests() { _claimsParser = new(); _claims = new(); } [Fact] public void Can_parse_claims_dictionary_access_string_returning_value_to_function() { // Arrange const string key = "CustomerId"; _claims.Add(new Claim(key, "1234")); // Act var result = _claimsParser.GetValue(_claims, key, default, default); // Assert ThenTheResultIs(result, new OkResponse("1234")); } [Fact] public void Should_return_error_response_when_cannot_find_requested_claim() { // Arrange const string key = "CustomerId"; _claims.Add(new Claim("BallsId", "1234")); // Act var result = _claimsParser.GetValue(_claims, key, default, default); // Assert ThenTheResultIs(result, new ErrorResponse(new List { new CannotFindClaimError($"Cannot find claim for key: {key}"), })); } [Fact] public void Can_parse_claims_dictionary_access_string_using_delimiter_and_retuning_at_correct_index() { // Arrange const string key = "Subject"; _claims.Add(new Claim("Subject", "registered|4321")); // Act var result = _claimsParser.GetValue(_claims, key, "|", 1); // Assert ThenTheResultIs(result, new OkResponse("4321")); } [Fact] public void Should_return_error_response_if_index_too_large() { // Arrange const string key = "Subject"; const string delimiter = "|"; const int index = 24; _claims.Add(new Claim("Subject", "registered|4321")); // Act var result = _claimsParser.GetValue(_claims, key, delimiter, index); // Assert ThenTheResultIs(result, new ErrorResponse(new List { new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}"), })); } [Fact] public void Should_return_error_response_if_index_too_small() { // Arrange const string key = "Subject"; const string delimiter = "|"; const int index = -1; _claims.Add(new Claim("Subject", "registered|4321")); // Act var result = _claimsParser.GetValue(_claims, key, delimiter, index); // Assert ThenTheResultIs(result, new ErrorResponse(new List { new CannotFindClaimError($"Cannot find claim for key: {key}, delimiter: {delimiter}, index: {index}"), })); } private static void ThenTheResultIs(Response actual, Response expected) { actual.Data.ShouldBe(expected.Data); actual.IsError.ShouldBe(expected.IsError); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/ConfigAwarePlaceholdersTests.cs ================================================ using Microsoft.Extensions.Configuration; using Ocelot.Infrastructure; using Ocelot.Responses; namespace Ocelot.UnitTests.Infrastructure; public class ConfigAwarePlaceholdersTests { private readonly ConfigAwarePlaceholders _placeholders; private readonly Mock _basePlaceholders; public ConfigAwarePlaceholdersTests() { var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile("appsettings.json"); var configuration = configurationBuilder.Build(); _basePlaceholders = new Mock(); _placeholders = new ConfigAwarePlaceholders(configuration, _basePlaceholders.Object); } [Fact] public void Should_return_value_from_underlying_placeholders() { // Arrange var baseUrl = "http://www.bbc.co.uk"; const string key = "{BaseUrl}"; _basePlaceholders.Setup(x => x.Get(key)).Returns(new OkResponse(baseUrl)); // Act var result = _placeholders.Get(key); // Assert result.Data.ShouldBe(baseUrl); } [Fact] public void Should_return_value_from_config_with_same_name_as_placeholder_if_underlying_placeholder_not_found() { // Arrange const string expected = "http://foo-bar.co.uk"; const string key = "{BaseUrl}"; _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); // Act var result = _placeholders.Get(key); // Assert result.Data.ShouldBe(expected); } [Theory] [InlineData("{TestConfig}")] [InlineData("{TestConfigNested:Child}")] public void Should_return_value_from_config(string key) { // Arrange const string expected = "foo"; _basePlaceholders.Setup(x => x.Get(key)).Returns(new ErrorResponse(new FakeError())); // Act var result = _placeholders.Get(key); // Assert result.Data.ShouldBe(expected); } [Fact] public void Should_call_underyling_when_added() { // Arrange const string key = "{Test}"; Func> func = () => new OkResponse("test)"); // Act _placeholders.Add(key, func); // Assert _basePlaceholders.Verify(p => p.Add(key, func), Times.Once); } [Fact] public void Should_call_underyling_when_removed() { // Arrange const string key = "{Test}"; // Act _placeholders.Remove(key); // Assert _basePlaceholders.Verify(p => p.Remove(key), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/ErrorListExtensionsTests.cs ================================================ using Ocelot.Errors; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class ErrorListExtensionsTests { private static readonly string NL = Environment.NewLine; [Fact] public void ToErrorString_ShouldReturnEmptyString_WhenListIsEmpty() { // Arrange var errors = new List(); // Act var result = errors.ToErrorString(); // Assert Assert.Equal(string.Empty, result); } [Fact] public void ToErrorString_ShouldReturnSingleError_WhenListHasOneItem() { // Arrange var errors = new List { new CannotAddPlaceholderError("First error") }; // Act var result = errors.ToErrorString(); // Assert Assert.Equal("CannotAddPlaceholderError: First error", result); } [Fact] public void ToErrorString_ShouldJoinMultipleErrors_WithNewLine() { // Arrange var errors = new List { new CannotAddPlaceholderError("First"), new CannotAddPlaceholderError("Second"), new CannotAddPlaceholderError("Third"), }; // Act var result = errors.ToErrorString(); // Assert var expected = $"CannotAddPlaceholderError: First{NL}CannotAddPlaceholderError: Second{NL}CannotAddPlaceholderError: Third"; Assert.Equal(expected, result); } [Fact] public void ToErrorString_ShouldInsertNewLineBefore_WhenBeforeIsTrue() { // Arrange var errors = new List { new CannotAddPlaceholderError("First") }; // Act var result = errors.ToErrorString(before: true); // Assert var expected = $"{NL}CannotAddPlaceholderError: First"; Assert.Equal(expected, result); } [Fact] public void ToErrorString_ShouldInsertNewLineAfter_WhenAfterIsTrue() { // Arrange var errors = new List { new CannotAddPlaceholderError("First") }; // Act var result = errors.ToErrorString(after: true); // Assert var expected = $"CannotAddPlaceholderError: First{NL}"; Assert.Equal(expected, result); } [Fact] public void ToErrorString_ShouldInsertNewLineBeforeAndAfter_WhenBothFlagsTrue() { // Arrange var errors = new List { new CannotAddPlaceholderError("First") }; // Act var result = errors.ToErrorString(before: true, after: true); // Assert var expected = $"{NL}CannotAddPlaceholderError: First{NL}"; Assert.Equal(expected, result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/HttpContextExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class HttpContextExtensionsTests { [Fact] public void IsOptionsMethod_ShouldReturnTrue_ForOptionsVerb() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = HttpMethod.Options.Method; // "OPTIONS" // Act var result = context.IsOptionsMethod(); // Assert Assert.True(result); } [Theory] [InlineData("GET")] [InlineData("POST")] [InlineData("PUT")] [InlineData("DELETE")] [InlineData("PATCH")] public void IsOptionsMethod_ShouldReturnFalse_ForNonOptionsVerbs(string method) { // Arrange var context = new DefaultHttpContext(); context.Request.Method = method; // Act var result = context.IsOptionsMethod(); // Assert Assert.False(result); } [Fact] public void IsOptionsMethod_ShouldBeCaseInsensitive() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = "options"; // lowercase // Act var result = context.IsOptionsMethod(); // Assert Assert.True(result); } [Fact] public void IsOptionsMethod_ShouldReturnFalse_WhenMethodIsEmpty() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = string.Empty; // Act var result = context.IsOptionsMethod(); // Assert Assert.False(result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/HttpRequestExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class HttpRequestExtensionsTests { [Fact] public void IsOptionsMethod_ShouldReturnTrue_ForOptionsVerb() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = HttpMethod.Options.Method; // "OPTIONS" // Act var result = context.Request.IsOptionsMethod(); // Assert Assert.True(result); } [Theory] [InlineData("GET")] [InlineData("POST")] [InlineData("PUT")] [InlineData("DELETE")] public void IsOptionsMethod_ShouldReturnFalse_ForNonOptionsVerbs(string method) { // Arrange var context = new DefaultHttpContext(); context.Request.Method = method; // Act var result = context.Request.IsOptionsMethod(); // Assert Assert.False(result); } [Fact] public void IsOptionsMethod_ShouldBeCaseInsensitive() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = "options"; // lowercase // Act var result = context.Request.IsOptionsMethod(); // Assert Assert.True(result); } [Fact] public void IsOptionsMethod_ShouldReturnFalse_WhenMethodIsEmpty() { // Arrange var context = new DefaultHttpContext(); context.Request.Method = string.Empty; // Act var result = context.Request.IsOptionsMethod(); // Assert Assert.False(result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/IEnumerableExtensionsTests.cs ================================================ using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class IEnumerableExtensionsTests { [Fact] public void ToHttpMethods_ShouldReturnEmptyHashSet_WhenCollectionIsNull() { // Arrange IEnumerable collection = null; // Act var result = collection.ToHttpMethods(); // Assert Assert.NotNull(result); Assert.Empty(result); } [Fact] public void ToHttpMethods_ShouldReturnEmptyHashSet_WhenCollectionIsEmpty() { // Arrange var collection = Enumerable.Empty(); // Act var result = collection.ToHttpMethods(); // Assert Assert.NotNull(result); Assert.Empty(result); } [Theory] [InlineData("GET")] [InlineData("POST")] [InlineData("PUT")] [InlineData("DELETE")] public void ToHttpMethods_ShouldReturnCorrectHttpMethod(string verb) { // Arrange var collection = new[] { verb }; // Act var result = collection.ToHttpMethods(); // Assert Assert.Single(result); Assert.Equal(verb, result.First().Method); } [Fact] public void ToHttpMethods_ShouldTrimWhitespace() { // Arrange var collection = new[] { " GET " }; // Act var result = collection.ToHttpMethods(); // Assert Assert.Single(result); Assert.Equal("GET", result.First().Method); } [Fact] public void ToHttpMethods_ShouldRemoveDuplicates() { // Arrange var collection = new[] { "GET", "GET", "POST" }; // Act var result = collection.ToHttpMethods(); // Assert Assert.Equal(2, result.Count); Assert.Contains(result, m => m.Method == "GET"); Assert.Contains(result, m => m.Method == "POST"); } [Fact] public void ToHttpMethods_ShouldHandleMixedVerbs() { // Arrange var collection = new[] { "GET", "POST", "PUT", "DELETE", "DELETE" }; // Act var result = collection.ToHttpMethods(); // Assert Assert.Equal(4, result.Count); Assert.Contains(result, m => m.Method == "GET"); Assert.Contains(result, m => m.Method == "POST"); Assert.Contains(result, m => m.Method == "PUT"); Assert.Contains(result, m => m.Method == "DELETE"); } // --------------------------- // Tests for NotNull // --------------------------- [Fact] public void NotNull_ShouldReturnEmptyEnumerable_WhenCollectionIsNull() { // Arrange IEnumerable collection = null; // Act var result = collection.NotNull(); // Assert Assert.NotNull(result); Assert.Empty(result); } [Fact] public void NotNull_ShouldReturnSameCollection_WhenNotNull() { // Arrange var collection = new[] { 1, 2, 3 }; // Act var result = collection.NotNull(); // Assert Assert.Same(collection, result); } // --------------------------- // Tests for Csv // --------------------------- [Fact] public void Csv_ShouldReturnEmptyString_WhenCollectionIsNull() { // Arrange IEnumerable values = null; // Act var result = values.Csv(); // Assert Assert.Equal(string.Empty, result); } [Fact] public void Csv_ShouldReturnEmptyString_WhenCollectionIsEmpty() { // Arrange var values = Enumerable.Empty(); // Act var result = values.Csv(); // Assert Assert.Equal(string.Empty, result); } [Fact] public void Csv_ShouldJoinValuesWithComma() { // Arrange var values = new[] { "one", "two", "three" }; // Act var result = values.Csv(); // Assert Assert.Equal("one,two,three", result); } [Fact] public void Csv_ShouldHandleSingleValue() { // Arrange var values = new[] { "only" }; // Act var result = values.Csv(); // Assert Assert.Equal("only", result); } [Fact] public void Csv_ShouldPreserveEmptyStrings() { // Arrange var values = new[] { "a", "", "b" }; // Act var result = values.Csv(); // Assert Assert.Equal("a,,b", result); } [Fact] public void Csv_ShouldTrimWhitespaceInsideValues() { // Arrange var values = new[] { " a ", " b ", "c" }; // Act var result = values.Csv(); // Assert // Note: Csv does not trim, so whitespace is preserved Assert.Equal(" a , b ,c", result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/Int32ExtensionsTests.cs ================================================ using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class Int32ExtensionsTests { // --------------------------- // Tests for Ensure // --------------------------- [Theory] [InlineData(-5, 0, 0)]// below default low [InlineData(5, 0, 5)] // above default low [InlineData(3, 2, 3)] // above custom low [InlineData(1, 5, 5)] // below custom low [InlineData(5, 5, 5)] // equal to low public void Ensure_ShouldReturnExpectedValue(int value, int low, int expected) { // Act var result = value.Ensure(low); // Assert Assert.True(result >= low); Assert.Equal(expected, result); } // --------------------------- // Tests for Positive (int) // --------------------------- [Theory] [InlineData(-10, 1)]// negative becomes 1 [InlineData(0, 1)] // zero becomes 1 [InlineData(5, 5)] // positive stays same public void Positive_Int_ShouldReturnPositiveValue(int value, int expected) { // Act var result = value.Positive(); // Assert Assert.True(result > 0); Assert.Equal(expected, result); } // --------------------------- // Tests for Positive (int?) // --------------------------- [Fact] public void Positive_NullableInt_ShouldReturnNull_WhenNoValue() { // Arrange int? value = null; // Act var result = value.Positive(); // Assert Assert.Null(result); } [Theory] [InlineData(-5, 1, 1)] // negative becomes default [InlineData(-5, 99, 99)] // negative becomes custom default [InlineData(0, 1, 1)] // zero becomes default [InlineData(10, 1, 10)] // positive stays same public void Positive_NullableInt_ShouldReturnPositiveValue(int? value, int toDefault, int expected) { // Act var result = value.Positive(toDefault); // Assert Assert.True(result > 0); Assert.Equal(expected, result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/StringBuilderExtensionsTests.cs ================================================ using Ocelot.Infrastructure.Extensions; using System.Text; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class StringBuilderExtensionsTests { [Fact] public void AppendNext_ShouldAppendWithoutSeparator_WhenBuilderIsEmpty() { // Arrange var sb = new StringBuilder(); // Act sb.AppendNext("first"); // Assert Assert.Equal("first", sb.ToString()); } [Fact] public void AppendNext_ShouldAppendWithSeparator_WhenBuilderHasContent() { // Arrange var sb = new StringBuilder("first"); // Act sb.AppendNext("second"); // Assert Assert.Equal("first,second", sb.ToString()); } [Fact] public void AppendNext_ShouldUseCustomSeparator() { // Arrange var sb = new StringBuilder("first"); // Act sb.AppendNext("second", ';'); // Assert Assert.Equal("first;second", sb.ToString()); } [Fact] public void AppendNext_ShouldAllowMultipleAppends() { // Arrange var sb = new StringBuilder(); // Act sb.AppendNext("one"); sb.AppendNext("two"); sb.AppendNext("three"); // Assert Assert.Equal("one,two,three", sb.ToString()); } [Fact] public void AppendNext_ShouldHandleEmptyStringAsNext() { // Arrange var sb = new StringBuilder("first"); // Act sb.AppendNext(""); // Assert Assert.Equal("first,", sb.ToString()); } [Fact] public void AppendNext_ShouldReturnSameBuilderInstance() { // Arrange var sb = new StringBuilder(); // Act var result = sb.AppendNext("test"); // Assert Assert.Same(sb, result); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/Extensions/StringExtensionsTests.cs ================================================ using Ocelot.Infrastructure.Extensions; namespace Ocelot.UnitTests.Infrastructure.Extensions; public class StringExtensionsTests { [Fact] public void TrimPrefix_ArgsCheck_ReturnedSource() { ((string)null).TrimPrefix("/").ShouldBeNull(); "x".TrimPrefix(null).ShouldBe("x"); "x".TrimPrefix(string.Empty).ShouldBe("x"); } [Fact] public void TrimPrefix_HasPrefix_HappyPath() { "/string".TrimPrefix("/").ShouldBe("string"); "///string".TrimPrefix("/").ShouldBe("string"); "ABABstring".TrimPrefix("AB").ShouldBe("string"); } [Fact] public void LastCharAsForwardSlash_HappyPath() { "string".LastCharAsForwardSlash().ShouldBe("string/"); "string/".LastCharAsForwardSlash().ShouldBe("string/"); } [Theory] [InlineData(0, "s")] [InlineData(1, "")] [InlineData(2, "s")] public void Plural_Int32(int count, string expected) { var actual = count.Plural(); Assert.Equal(expected, actual); } [Theory] [InlineData("item", 0, "items")] [InlineData("item", 1, "item")] [InlineData("item", 2, "items")] public void Plural_ThisString(string source, int count, string expected) { var actual = source.Plural(count); Assert.Equal(expected, actual); } [Theory] [InlineData(null, true)] [InlineData("", true)] [InlineData(" ", true)] [InlineData("x", false)] public void IsEmpty(string str, bool expected) { bool actual = str.IsEmpty(); Assert.Equal(expected, actual); } [Theory] [InlineData(null, false)] [InlineData("", false)] [InlineData(" ", false)] [InlineData("x", true)] public void IsNotEmpty(string str, bool expected) { bool actual = str.IsNotEmpty(); Assert.Equal(expected, actual); } [Theory] [InlineData(null, "def", "def")] [InlineData("", "def", "def")] [InlineData(" ", "def", "def")] [InlineData("x", "def", "x")] public void IfEmpty(string str, string def, string expected) { var actual = str.IfEmpty(def); Assert.Equal(expected, actual); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/HttpDataRepositoryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.RequestData; using Ocelot.Responses; namespace Ocelot.UnitTests.Infrastructure; public class HttpDataRepositoryTests : UnitTest { private readonly DefaultHttpContext _httpContext; private readonly IHttpContextAccessor _httpContextAccessor; private readonly HttpDataRepository _repository; private object _result; public HttpDataRepositoryTests() { _httpContext = new DefaultHttpContext(); _httpContextAccessor = new HttpContextAccessor { HttpContext = _httpContext }; _repository = new HttpDataRepository(_httpContextAccessor); } /* TODO - Additional tests -> Type mistmatch aka Add string, request int TODO - Additional tests -> HttpContent null. This should never happen */ [Fact] public void Get_returns_correct_key_from_http_context() { // Arrange _httpContext.Items.Add("key", "string"); // Act _result = _repository.Get("key"); // Assert ThenTheResultIsAnOkResponse("string"); } [Fact] public void Get_returns_error_response_if_the_key_is_not_found() //Therefore does not return null { // Arrange _httpContext.Items.Add("key", "string"); // Act _result = _repository.Get("keyDoesNotExist"); // Assert ThenTheResultIsAnErrorReposnse(); } [Fact] public void Should_update() { // Arrange _httpContext.Items.Add("key", "string"); _repository.Update("key", "new string"); // Act _result = _repository.Get("key"); // Assert ThenTheResultIsAnOkResponse("new string"); } private void ThenTheResultIsAnErrorReposnse() { _result.ShouldBeOfType>(); ((ErrorResponse)_result).Data.ShouldBe(default); ((ErrorResponse)_result).IsError.ShouldBe(true); ((ErrorResponse)_result).Errors.ShouldHaveSingleItem() .ShouldBeOfType() .Message.ShouldStartWith("Unable to find data for key: "); } private void ThenTheResultIsAnOkResponse(object resultValue) { _result.ShouldBeOfType>(); ((OkResponse)_result).Data.ShouldBe(resultValue); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/InMemoryBusTests.cs ================================================ using Ocelot.Infrastructure; namespace Ocelot.UnitTests.Infrastructure; public class InMemoryBusTests { private readonly InMemoryBus _bus = new(); [Fact] public async Task Should_publish_with_delay() { // Arrange var called = false; _bus.Subscribe(x => called = true); // Act _bus.Publish(new object(), 1); await Task.Delay(100, TestContext.Current.CancellationToken); // Assert called.ShouldBeTrue(); } [Fact] public void Should_not_be_publish_yet_as_no_delay_in_caller() { // Arrange var called = false; _bus.Subscribe(x => called = true); // Act _bus.Publish(new object(), 1); // Assert called.ShouldBeFalse(); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/PlaceholdersTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; namespace Ocelot.UnitTests.Infrastructure; public class PlaceholdersTests { private readonly Mock _finder; private readonly Mock _repo; private readonly Mock _accessor; /*private readonly Mock _loggerFactory; private readonly Mock _logger;*/ private readonly Placeholders _placeholders; public PlaceholdersTests() { _finder = new Mock(); _repo = new Mock(); _accessor = new Mock(); //_loggerFactory = new Mock(); _placeholders = new Placeholders(_finder.Object, _repo.Object, _accessor.Object); } [Fact] public void Should_return_base_url() { // Arrange var baseUrl = "http://www.bbc.co.uk"; _finder.Setup(x => x.Find()).Returns(baseUrl); // Act var result = _placeholders.Get("{BaseUrl}"); // Assert result.Data.ShouldBe(baseUrl); } [Fact] [Trait("Feat", "623")] // https://github.com/ThreeMammals/Ocelot/issues/623 [Trait("PR", "632")] // https://github.com/ThreeMammals/Ocelot/pull/632 public void Should_return_remote_ip_address() { // Arrange var httpContext = new DefaultHttpContext { Connection = { RemoteIpAddress = IPAddress.Any } }; _accessor.Setup(x => x.HttpContext).Returns(httpContext); // Act var result = _placeholders.Get("{RemoteIpAddress}"); // Assert result.Data.ShouldBe(httpContext.Connection.RemoteIpAddress.ToString()); } [Fact] public void Should_return_key_does_not_exist() { // Arrange, Act var result = _placeholders.Get("{Test}"); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); } [Fact] public void Should_return_downstream_base_url_when_port_is_not_80_or_443() { // Arrange var httpRequest = new HttpRequestMessage { RequestUri = new("http://www.bbc.co.uk"), }; var request = new DownstreamRequest(httpRequest); // Act var result = _placeholders.Get("{DownstreamBaseUrl}", request); // Assert result.Data.ShouldBe("http://www.bbc.co.uk/"); } [Fact] public void Should_return_downstream_base_url_when_port_is_80_or_443() { // Arrange var httpRequest = new HttpRequestMessage { RequestUri = new("http://www.bbc.co.uk:123"), }; var request = new DownstreamRequest(httpRequest); // Act var result = _placeholders.Get("{DownstreamBaseUrl}", request); // Assert result.Data.ShouldBe("http://www.bbc.co.uk:123/"); } [Fact] public void Should_return_key_does_not_exist_for_http_request_message() { // Arrange var request = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://west.com")); // Act var result = _placeholders.Get("{Test}", request); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Unable to find placeholder called {Test}"); } [Fact] public void Should_return_trace_id() { // Arrange var traceId = "123"; _repo.Setup(x => x.Get("TraceId")).Returns(new OkResponse(traceId)); // Act var result = _placeholders.Get("{TraceId}"); // Assert result.Data.ShouldBe(traceId); } [Fact] public void Should_return_ok_when_added() { // Arrange, Act var result = _placeholders.Add("{Test}", () => new OkResponse("test")); // Assert result.IsError.ShouldBeFalse(); } [Fact] public void Should_return_ok_when_removed() { // Arrange var result = _placeholders.Add("{Test}", () => new OkResponse("test")); // Act result = _placeholders.Remove("{Test}"); // Assert result.IsError.ShouldBeFalse(); } [Fact] public void Should_return_error_when_added() { // Arrange var result = _placeholders.Add("{Test}", () => new OkResponse("test")); // Act result = _placeholders.Add("{Test}", () => new OkResponse("test")); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Unable to add placeholder: {Test}, placeholder already exists"); } [Fact] public void Should_return_error_when_removed() { // Arrange, Act var result = _placeholders.Remove("{Test}"); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Unable to remove placeholder: {Test}, placeholder does not exists"); } [Fact] public void Should_return_upstreamHost() { // Arrange var upstreamHost = "UpstreamHostA"; var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Append("Host", upstreamHost); _accessor.Setup(x => x.HttpContext).Returns(httpContext); // Act var result = _placeholders.Get("{UpstreamHost}"); // Assert result.Data.ShouldBe(upstreamHost); } [Fact] public void Should_return_error_when_finding_upstbecause_Host_not_set() { // Arrange var httpContext = new DefaultHttpContext(); _accessor.Setup(x => x.HttpContext).Returns(httpContext); // Act var result = _placeholders.Get("{UpstreamHost}"); // Assert result.IsError.ShouldBeTrue(); } [Fact] public void Should_return_error_when_finding_upstream_host_because_exception_thrown() { // Arrange _accessor.Setup(x => x.HttpContext).Throws(new Exception()); // Act var result = _placeholders.Get("{UpstreamHost}"); // Assert result.IsError.ShouldBeTrue(); } [Fact] public void Get_GetRemoteIpAddress() { // Arrange var httpContext = new DefaultHttpContext { Connection = { RemoteIpAddress = IPAddress.Any } }; _accessor.Setup(x => x.HttpContext).Returns(httpContext); // Act var result = _placeholders.Get("{RemoteIpAddress}"); // Assert result.Data.ShouldBe(httpContext.Connection.RemoteIpAddress.ToString()); } } ================================================ FILE: test/Ocelot.UnitTests/Infrastructure/ScopesAuthorizerTests.cs ================================================ using Ocelot.Authorization; using Ocelot.Errors; using Ocelot.Infrastructure.Claims; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.Infrastructure; public class ScopesAuthorizerTests : UnitTest { private readonly ScopesAuthorizer _authorizer; private readonly Mock _parser; public ScopesAuthorizerTests() { _parser = new Mock(); _authorizer = new ScopesAuthorizer(_parser.Object); } [Fact] public void Should_return_ok_if_no_allowed_scopes() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List(); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] public void Should_return_ok_if_null_allowed_scopes() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = (List)null; // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] public void Should_return_error_if_claims_parser_returns_error() { // Arrange var fakeError = new FakeError(); var principal = new ClaimsPrincipal(); GivenTheParserReturns(new ErrorResponse>(fakeError)); var allowedScopes = new List { "doesntmatter" }; // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new ErrorResponse(fakeError)); } [Fact] public void Should_match_scopes_and_return_ok_result() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "someScope" }; GivenTheParserReturns(new OkResponse>(allowedScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] public void Should_not_match_scopes_and_return_error_result() { // Arrange var fakeError = new FakeError(); var principal = new ClaimsPrincipal(); var allowedScopes = new List { "someScope" }; var userScopes = new List { "anotherScope" }; GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new ErrorResponse(fakeError)); } #region PR 1478 [Fact] [Trait("Bug", "913")] // https://github.com/ThreeMammals/Ocelot/issues/913 [Trait("PR", "1478")] // https://github.com/ThreeMammals/Ocelot/pull/1478 public void Should_split_space_separated_scope_and_match() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "api.read", "api.write" }; var userScopes = new List { "api.read api.write openid" }; // Space-separated scope claim GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] public void Should_split_space_separated_scope_and_match_single_scope() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "api.write" }; var userScopes = new List { "api.read api.write openid" }; // Space-separated scope claim GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] public void Should_split_space_separated_scope_but_not_match() { // Arrange var fakeError = new FakeError(); var principal = new ClaimsPrincipal(); var allowedScopes = new List { "admin" }; var userScopes = new List { "api.read api.write openid" }; // Space-separated scope claim GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new ErrorResponse(fakeError)); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] public void Should_handle_multiple_scope_claims_without_splitting() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "api.read" }; var userScopes = new List { "api.read", "api.write" }; // Multiple separate claims GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] public void Should_not_split_single_scope_without_spaces() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "api.read" }; var userScopes = new List { "api.read" }; // Single scope without spaces GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } [Fact] [Trait("Bug", "913")] [Trait("PR", "1478")] public void Should_handle_empty_string_after_splitting() { // Arrange var principal = new ClaimsPrincipal(); var allowedScopes = new List { "api.read" }; var userScopes = new List { " api.read api.write " }; // Scope with extra spaces GivenTheParserReturns(new OkResponse>(userScopes)); // Act var result = _authorizer.Authorize(principal, allowedScopes); // Assert ThenTheFollowingIsReturned(result, new OkResponse(true)); } #endregion PR 1478 private void GivenTheParserReturns(Response> response) { _parser.Setup(x => x.GetValuesByClaimType(It.IsAny>(), It.IsAny())).Returns(response); } private static void ThenTheFollowingIsReturned(Response actual, Response expected) { actual.Data.ShouldBe(expected.Data); actual.IsError.ShouldBe(expected.IsError); } } public class FakeError : Error { public FakeError() : base("fake error", OcelotErrorCode.CannotAddDataError, 404) { } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/EndpointClientV1Tests.cs ================================================ using KubeClient; using KubeClient.Http; using KubeClient.Http.Formatters; using KubeClient.Models; using Microsoft.Extensions.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "2168")] [Trait("PR", "2174")] // https://github.com/ThreeMammals/Ocelot/pull/2174 public class EndpointClientV1Tests { private readonly EndPointClientV1 _endpointClient; private readonly Mock _kubeApiClient = new(); public EndpointClientV1Tests() { var loggerFactory = new Mock(); loggerFactory.Setup(x => x.CreateLogger(It.IsAny())) .Returns(new Mock().Object); _kubeApiClient.Setup(x => x.LoggerFactory) .Returns(loggerFactory.Object); _endpointClient = new EndPointClientV1(_kubeApiClient.Object); _kubeApiClient.Setup(x => x.ResourceClient(It.IsAny>())) .Returns(_endpointClient); } [Theory] [InlineData(null)] [InlineData("")] public async Task GetAsync_WhenServiceIsNullOrEmpty_ThrowsArgumentException(string serviceName) { // Act var watchCall = () => _endpointClient.GetAsync(serviceName, null, CancellationToken.None); // Assert var e = await watchCall.ShouldThrowAsync(); e.ParamName.ShouldBe(nameof(serviceName)); } [Theory] [InlineData(null)] [InlineData("test-namespace")] public async Task GetAsync_KubeNamespaceChanges_HappyPath(string kubeNamespace) { // Arrange using var client = new FakeHttpClient() { BaseAddress = new UriBuilder(Uri.UriSchemeHttp, "localhost", 1234).Uri, }; _kubeApiClient.SetupGet(x => x.Http).Returns(client); _kubeApiClient.SetupGet(x => x.DefaultNamespace).Returns(nameof(EndpointClientV1Tests)); // Act var endpoints = await _endpointClient.GetAsync("service-XYZ", kubeNamespace, CancellationToken.None); // Assert Assert.NotNull(endpoints); Assert.Equal(nameof(GetAsync_KubeNamespaceChanges_HappyPath), endpoints.Kind); var url = client.Request.RequestUri.AbsoluteUri; Assert.Contains(kubeNamespace ?? nameof(EndpointClientV1Tests), url); Assert.Contains("service-XYZ", url); client.Request.Options.TryGetValue(new("KubeClient.Http.Request"), out HttpRequest request); Assert.NotNull(request?.TemplateParameters); Assert.True(request.TemplateParameters.ContainsKey("Namespace")); Assert.True(request.TemplateParameters.ContainsKey("ServiceName")); } [Theory] [InlineData(null)] [InlineData("")] public void Watch_WhenServiceIsNullOrEmpty_ThrowsArgumentException(string serviceName) { // Act var watchCall = () => _endpointClient.Watch(serviceName, null, CancellationToken.None); // Assert watchCall.ShouldThrow().ParamName.ShouldBe(nameof(serviceName)); } [Fact] public void Watch_ProvidesObservable() { // Act var observable = _endpointClient.Watch("some-service", null, CancellationToken.None); // Assert observable.ShouldNotBeNull(); } } internal class FakeHttpClient : HttpClient, IDisposable { private readonly Mock formatters = new(); private readonly Mock formatter = new(); private readonly List disposables = new(); public FakeHttpClient([CallerMemberName] string testName = null) { formatter.Setup(x => x.ReadAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(() => new EndpointsV1() { Kind = testName }); formatters.SetupGet(x => x.Count).Returns(1); formatters.Setup(x => x.FindInputFormatter(It.IsAny())) .Returns(formatter.Object); } public new void Dispose() { disposables.ForEach(d => d.Dispose()); base.Dispose(); } public HttpRequestMessage Request { get; private set; } public override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { Request = request; HttpResponseMessage response = new() { StatusCode = HttpStatusCode.OK, RequestMessage = new(), }; response.RequestMessage.Properties.Add(MessageProperties.ContentFormatters, formatters.Object); response.Content = new StringContent("Hello from " + nameof(FakeHttpClient)); disposables.Add(response); disposables.Add(response.Content); return Task.FromResult(response); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/FakeKubeApiClientFactory.cs ================================================ using KubeClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Provider.Kubernetes; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace Ocelot.UnitTests.Kubernetes; internal class FakeKubeApiClientFactory : KubeApiClientFactory { public FakeKubeApiClientFactory(ILoggerFactory logger, IOptions options) : base(logger, options) { } public FakeKubeApiClientFactory(ILoggerFactory logger, IOptions options, string serviceAccountPath) : base(logger, options) { ServiceAccountPath = serviceAccountPath; } public FakeKubeApiClientFactory(string serviceAccountPath) : base(Mock.Of(), Mock.Of>()) { ServiceAccountPath = serviceAccountPath; } public string ActualServiceAccountPath => ServiceAccountPath; public KubeApiClient Actual { get; private set; } public override KubeApiClient Get(bool usePodServiceAccount) { return Actual = base.Get(usePodServiceAccount); } public static async Task CreateCertificate(string crtFile) { var certificate = CreateCertificate(); byte[] certBytes = certificate.Export(X509ContentType.Cert); await File.WriteAllBytesAsync(crtFile, certBytes); } public static X509Certificate2 CreateCertificate() { // Generate a self-signed certificate using RSA rsa = RSA.Create(2048); var request = new CertificateRequest("CN=MyCertificate", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // Add extensions to the certificate (optional) request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, false)); request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false)); return request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/KubeApiClientFactoryTests.cs ================================================ using KubeClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Ocelot.Provider.Kubernetes; namespace Ocelot.UnitTests.Kubernetes; public sealed class KubeApiClientFactoryTests : KubeApiClientFactoryTestsBase { [Theory] [Trait("Bug", "2299")] [InlineData(null)] [InlineData("")] public void ServiceAccountPath_NoValue_FallbackedToDefValue(string serviceAccountPath) { // Arrange var s = new FakeKubeApiClientFactory(serviceAccountPath); // Act var actual = s.ActualServiceAccountPath; // Assert actual.ShouldNotBeNullOrEmpty(); Assert.Equal(KubeClientConstants.DefaultServiceAccountPath, actual); } } [Collection(nameof(SequentialTests))] public class KubeApiClientFactorySequentialTests : KubeApiClientFactoryTestsBase { [Fact] [Trait("Bug", "2299")] public async Task Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount() { // Arrange var serviceAccountPath = Path.Combine(AppContext.BaseDirectory, TestID); var stub = new FakeKubeApiClientFactory(logger.Object, options.Object, serviceAccountPath); var expectedHost = IPAddress.Loopback.ToString(); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", expectedHost); int expectedPort = PortFinder.GetRandomPort(); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", expectedPort.ToString()); folders.Add(serviceAccountPath); if (!Directory.Exists(serviceAccountPath)) { Directory.CreateDirectory(serviceAccountPath); } var path = Path.Combine(serviceAccountPath, "namespace"); await File.WriteAllTextAsync(path, nameof(Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount), TestContext.Current.CancellationToken); files.Add(path); path = Path.Combine(serviceAccountPath, "token"); await File.WriteAllTextAsync(path, TestID, TestContext.Current.CancellationToken); files.Add(path); path = Path.Combine(serviceAccountPath, "ca.crt"); await FakeKubeApiClientFactory.CreateCertificate(path); files.Add(path); var log = new Mock(); logger.Setup(x => x.CreateLogger(It.IsAny())).Returns(log.Object); // Act const bool UsePodServiceAccount = true; var actual = stub.Get(UsePodServiceAccount); // ! // Assert actual.ShouldNotBeNull().ShouldBeOfType(); actual.ApiEndPoint.ShouldNotBeNull(); actual.ApiEndPoint.Host.ShouldBe(expectedHost); actual.ApiEndPoint.Port.ShouldBe(expectedPort); actual.DefaultNamespace.ShouldNotBeNull(nameof(Get_UsePodServiceAccount_ShouldCreateFromPodServiceAccount)); actual.LoggerFactory.ShouldNotBeNull(); actual.LoggerFactory.ShouldBe(logger.Object); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", null); } } public class KubeApiClientFactoryTestsBase : FileUnitTest { protected readonly Mock logger = new(); protected readonly Mock> options = new(); protected KubeApiClientFactory sut; public KubeApiClientFactoryTestsBase() { sut = new(logger.Object, options.Object); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/KubeServiceBuilderTests.cs ================================================ using KubeClient.Models; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Values; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "1967")] public sealed class KubeServiceBuilderTests { private readonly Mock factory; private readonly Mock serviceCreator; private readonly Mock logger; private KubeServiceBuilder sut; public KubeServiceBuilderTests() { factory = new(); serviceCreator = new(); logger = new(); } private void Arrange() { factory.Setup(x => x.CreateLogger()) .Returns(logger.Object) .Verifiable(); logger.Setup(x => x.LogDebug(It.IsAny>())) .Verifiable(); sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] public void Cstor_NullArgs_ThrownException(bool isFactory, bool isServiceCreator) { // Arrange var arg1 = isFactory ? factory.Object : null; var arg2 = isServiceCreator ? serviceCreator.Object : null; // Act, Assert Assert.Throws( arg1 is null ? "factory" : arg2 is null ? "serviceCreator" : string.Empty, () => sut = new KubeServiceBuilder(arg1, arg2)); } [Fact] public void Cstor_NotNullArgs_ObjCreated() { // Arrange factory.Setup(x => x.CreateLogger()).Verifiable(); // Act sut = new KubeServiceBuilder(factory.Object, serviceCreator.Object); // Assert Assert.NotNull(sut); factory.Verify(x => x.CreateLogger(), Times.Once()); } [Theory] [InlineData(false, false)] [InlineData(false, true)] [InlineData(true, false)] public void BuildServices_NullArgs_ThrownException(bool isConfiguration, bool isEndpoint) { // Arrange var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; var arg2 = isEndpoint ? new EndpointsV1() : null; Arrange(); // Act, Assert Assert.Throws( arg1 is null ? "configuration" : arg2 is null ? "endpoint" : string.Empty, () => _ = sut.BuildServices(arg1, arg2)); } [Theory] [InlineData(0)] [InlineData(1)] [InlineData(2)] public void BuildServices_WithSubsets_SelectedManyServicesPerSubset(int subsetCount) { // Arrange var configuration = new KubeRegistryConfiguration(); var endpoint = new EndpointsV1(); for (int i = 1; i <= subsetCount; i++) { var subset = new EndpointSubsetV1(); subset.Addresses.Add(new() { NodeName = "subset" + i, Hostname = i.ToString() }); endpoint.Subsets.Add(subset); } serviceCreator.Setup(x => x.Create(configuration, endpoint, It.IsAny())) .Returns((c, e, s) => { var item = s.Addresses[0]; int count = int.Parse(item.Hostname); var list = new List(count); while (count > 0) { var id = count--.ToString(); list.Add(new Service($"{item.NodeName}-service{id}", null, id, id, null)); } return list; }); var many = endpoint.Subsets.Sum(s => int.Parse(s.Addresses[0].Hostname)); Arrange(); // Act var actual = sut.BuildServices(configuration, endpoint); // Assert Assert.NotNull(actual); var l = actual.ToList(); Assert.Equal(many, l.Count); serviceCreator.Verify(x => x.Create(configuration, endpoint, It.IsAny()), Times.Exactly(endpoint.Subsets.Count)); logger.Verify(x => x.LogDebug(It.IsAny>()), Times.Once()); } [Theory] [InlineData(false, false, false, false, "K8s '?:?:?' endpoint: Total built 0 services.")] [InlineData(false, false, false, true, "K8s '?:?:?' endpoint: Total built 0 services.")] [InlineData(false, false, true, false, "K8s '?:?:?' endpoint: Total built 0 services.")] [InlineData(false, false, true, true, "K8s '?:?:Name' endpoint: Total built 0 services.")] [InlineData(false, true, true, true, "K8s '?:ApiVersion:Name' endpoint: Total built 0 services.")] [InlineData(true, true, true, true, "K8s 'Kind:ApiVersion:Name' endpoint: Total built 0 services.")] public void BuildServices_WithEndpoint_LogDebug(bool hasKind, bool hasApiVersion, bool hasMetadata, bool hasMetadataName, string message) { // Arrange var configuration = new KubeRegistryConfiguration(); var endpoint = new EndpointsV1() { Kind = hasKind ? nameof(EndpointsV1.Kind) : null, ApiVersion = hasApiVersion ? nameof(EndpointsV1.ApiVersion) : null, Metadata = hasMetadata ? new() { Name = hasMetadataName ? nameof(ObjectMetaV1.Name) : null, } : null, }; Arrange(); string actualMesssage = null; logger.Setup(x => x.LogDebug(It.IsAny>())) .Callback>(f => actualMesssage = f.Invoke()); // Act var actual = sut.BuildServices(configuration, endpoint); // Assert Assert.NotNull(actual); logger.Verify(x => x.LogDebug(It.IsAny>()), Times.Once()); Assert.NotNull(actualMesssage); Assert.Equal(message, actualMesssage); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/KubeServiceCreatorTests.cs ================================================ using KubeClient.Models; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "1967")] public sealed class KubeServiceCreatorTests { private readonly Mock factory; private readonly Mock logger; private KubeServiceCreator sut; public KubeServiceCreatorTests() { factory = new(); logger = new(); } private void Arrange() { factory.Setup(x => x.CreateLogger()) .Returns(logger.Object) .Verifiable(); logger.Setup(x => x.LogDebug(It.IsAny>())) .Verifiable(); sut = new KubeServiceCreator(factory.Object); } [Fact] public void Cstor_NullArg_ThrownException() { // Arrange, Act, Assert Assert.Throws("factory", () => sut = new KubeServiceCreator(null)); } [Fact] public void Cstor_NotNullArg_ObjCreated() { // Arrange factory.Setup(x => x.CreateLogger()).Verifiable(); // Act sut = new KubeServiceCreator(factory.Object); // Assert Assert.NotNull(sut); factory.Verify(x => x.CreateLogger(), Times.Once()); } [Theory] [InlineData(false, true, true)] [InlineData(true, false, true)] [InlineData(true, true, false)] public void Create_NullArgs_ReturnedEmpty(bool isConfiguration, bool isEndpoint, bool isSubset) { // Arrange var arg1 = isConfiguration ? new KubeRegistryConfiguration() : null; var arg2 = isEndpoint ? new EndpointsV1() : null; var arg3 = isSubset ? new EndpointSubsetV1() : null; Arrange(); // Act var actual = sut.Create(arg1, arg2, arg3); // Assert Assert.NotNull(actual); Assert.Empty(actual); } [Trait("Bug", "977")] [Fact(DisplayName = "Create: With empty args -> No exceptions during creation")] public void Create_NotNullButEmptyArgs_CreatedEmptyService() { // Arrange var arg1 = new KubeRegistryConfiguration() { KubeNamespace = nameof(KubeServiceCreatorTests), KeyOfServiceInK8s = nameof(Create_NotNullButEmptyArgs_CreatedEmptyService), }; var arg2 = new EndpointsV1(); var arg3 = new EndpointSubsetV1(); arg3.Addresses.Add(new()); arg2.Subsets.Add(arg3); Arrange(); // Act var actual = sut.Create(arg1, arg2, arg3); // Assert Assert.NotNull(actual); Assert.NotEmpty(actual); var actualService = actual.SingleOrDefault(); Assert.NotNull(actualService); Assert.Null(actualService.Name); Assert.NotNull(actualService.HostAndPort); Assert.Equal(80, actualService.HostAndPort.DownstreamPort); } [Fact] public void Create_ValidArgs_HappyPath() { // Arrange var arg1 = new KubeRegistryConfiguration() { KubeNamespace = nameof(KubeServiceCreatorTests), KeyOfServiceInK8s = nameof(Create_ValidArgs_HappyPath), Scheme = "happy", //nameof(HttpScheme.Http), }; var arg2 = new EndpointsV1() { ApiVersion = "v1", Metadata = new() { Namespace = nameof(KubeServiceCreatorTests), Name = nameof(Create_ValidArgs_HappyPath), Uid = Guid.NewGuid().ToString(), }, }; var arg3 = new EndpointSubsetV1(); arg3.Addresses.Add(new() { Ip = "8.8.8.8", NodeName = "google", Hostname = "dns.google", }); var ports = new List { new() { Name = nameof(HttpScheme.Http), Port = 80 }, new() { Name = "happy", Port = 888 }, }; arg3.Ports.AddRange(ports); arg2.Subsets.Add(arg3); Arrange(); // Act var actual = sut.Create(arg1, arg2, arg3); // Assert Assert.NotNull(actual); Assert.NotEmpty(actual); var service = actual.SingleOrDefault(); Assert.NotNull(service); Assert.Equal(nameof(Create_ValidArgs_HappyPath), service.Name); Assert.Equal("happy", service.HostAndPort.Scheme); Assert.Equal(888, service.HostAndPort.DownstreamPort); Assert.Equal("8.8.8.8", service.HostAndPort.DownstreamHost); logger.Verify(x => x.LogDebug(It.IsAny>()), Times.Once()); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/KubernetesProviderFactoryTests.cs ================================================ using KubeClient; using KubeClient.Models; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Kubernetes; public sealed class KubernetesProviderFactoryTests : KubernetesProviderFactoryTestsBase { [Theory] [Trait("Bug", "977")] [InlineData(typeof(Kube))] [InlineData(typeof(PollKube))] [InlineData(typeof(WatchKube))] public void CreateProvider_ClientHasOriginalLifetimeWithEnabledScopesValidation_ShouldResolveProvider(Type providerType) { // Arrange _builder.AddKubernetes(); var endpointClient = new Mock(); endpointClient.Setup(x => x.Watch(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Mock.Of>>()); var kubeClient = new Mock(); kubeClient.Setup(x => x.ResourceClient(It.IsAny>())) .Returns(endpointClient.Object); var descriptor = _builder.Services.First(x => x.ServiceType == typeof(IKubeApiClient)); _builder.Services.Replace(ServiceDescriptor.Describe(descriptor.ServiceType, _ => kubeClient.Object, descriptor.Lifetime)); // Act var actual = CreateProvider(providerType.Name); // Assert actual.ShouldNotBeNull().ShouldBeOfType(providerType); } [Theory] [Trait("Bug", "977")] [InlineData(nameof(Kube))] [InlineData(nameof(PollKube))] [InlineData(nameof(WatchKube))] public void CreateProvider_ClientHasScopedLifetimeWithEnabledScopesValidation_ShouldFailToResolve(string providerType) { // Arrange _builder.AddKubernetes(); var descriptor = ServiceDescriptor.Describe(typeof(IKubeApiClient), _ => Mock.Of(), ServiceLifetime.Scoped); _builder.Services.Replace(descriptor); // Act IServiceDiscoveryProvider actual = null; var func = () => actual = CreateProvider(providerType); // Assert var ex = func.ShouldThrow(); ex.Message.ShouldContain("Cannot resolve scoped service 'KubeClient.IKubeApiClient' from root provider"); actual.ShouldBeNull(); } [Fact] [Trait("Feat", "2256")] public void CreateProvider_KubeApiClientFactory_ShouldCreateFromOptions() { // Arrange _builder.AddKubernetes(false); // !!! // In app user must setup by the following: //MyOptions options = new(); //_builder.Configuration.GetSection(nameof(MyOptions)).Bind(options); var options = new Mock>(); options.SetupGet(x => x.Value).Returns(new KubeClientOptions { ApiEndPoint = new UriBuilder(Uri.UriSchemeHttps, IPAddress.Loopback.ToString(), PortFinder.GetRandomPort()).Uri, ClientCertificate = FakeKubeApiClientFactory.CreateCertificate(), KubeNamespace = nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromOptions), }); _builder.Services.AddSingleton>(options.Object); // Act var actual = CreateProvider(nameof(Kube)); // Assert actual.ShouldNotBeNull().ShouldBeOfType(); } [Fact] [Trait("Feat", "2256")] public void CreateProvider_HasConfigureOptions_ShouldCallConfigure() { // Arrange _builder.AddKubernetes(configureOptions: null, username: "myUser"); // !!! // Act, Assert var actual = CreateProvider(nameof(Kube)); actual.ShouldNotBeNull().ShouldBeOfType(); // Act, Assert var provider = _builder.Services.BuildServiceProvider(true); var o = provider.GetService>().ShouldNotBeNull(); o.Value.ShouldNotBeNull().Username.ShouldBe("myUser"); // Act, Assert var configureOptions = provider.GetService>().ShouldNotBeNull(); var opts = new KubeClientOptions(); configureOptions.Configure(opts); opts.Username.ShouldBe("myUser"); } } [Collection(nameof(SequentialTests))] public sealed class KubernetesProviderFactorySequentialTests : KubernetesProviderFactoryTestsBase { [Fact] [Trait("Feat", "2256")] public async Task CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount() { // Arrange _builder.AddKubernetes(true); // !!! var serviceAccountPath = Path.Combine(AppContext.BaseDirectory, TestID); var stub = new FakeKubeApiClientFactory(null, null, serviceAccountPath); var original = _builder.Services.First(x => x.ServiceType == typeof(IKubeApiClientFactory)); var descriptor = ServiceDescriptor.Describe(original.ServiceType, _ => stub, original.Lifetime); _builder.Services.Replace(descriptor); var expectedHost = IPAddress.Loopback.ToString(); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", expectedHost); int expectedPort = PortFinder.GetRandomPort(); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", expectedPort.ToString()); folders.Add(serviceAccountPath); if (!Directory.Exists(serviceAccountPath)) { Directory.CreateDirectory(serviceAccountPath); } var path = Path.Combine(serviceAccountPath, "namespace"); await File.WriteAllTextAsync(path, nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount), TestContext.Current.CancellationToken); files.Add(path); path = Path.Combine(serviceAccountPath, "token"); await File.WriteAllTextAsync(path, TestID, TestContext.Current.CancellationToken); files.Add(path); path = Path.Combine(serviceAccountPath, "ca.crt"); await FakeKubeApiClientFactory.CreateCertificate(path); files.Add(path); // Act var actualProvider = CreateProvider(nameof(Kube)); // Assert actualProvider.ShouldNotBeNull().ShouldBeOfType(); stub.ShouldNotBeNull(); stub.Actual.ShouldNotBeNull(); stub.Actual.ApiEndPoint.ShouldNotBeNull(); stub.Actual.ApiEndPoint.Host.ShouldBe(expectedHost); stub.Actual.ApiEndPoint.Port.ShouldBe(expectedPort); stub.Actual.DefaultNamespace.ShouldNotBeNull(nameof(CreateProvider_KubeApiClientFactory_ShouldCreateFromPodServiceAccount)); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", null); } [Fact] [Trait("Bug", "2299")] public void Bug2299_StepsToReproduce_ShouldNotThrowExceptionByPathCombine() { // Arrange _builder.AddKubernetes(); // !!! Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", "localhost"); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", PortFinder.GetRandomPort().ToString()); // Act var ex = Assert.ThrowsAny( () => CreateProvider(nameof(Kube))); // Assert ex.ShouldNotBeOfType(); ex.ShouldBeOfType(); ex.StackTrace.ShouldContain("at KubeClient.KubeClientOptions.FromPodServiceAccount(String serviceAccountPath)"); ex.StackTrace.ShouldNotContain("at System.IO.Path.Combine(String path1, String path2)"); ex.Message.ShouldNotBe("Value cannot be null. (Parameter 'path1')"); Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_PORT", null); } } public class KubernetesProviderFactoryTestsBase : FileUnitTest { protected readonly IOcelotBuilder _builder; public KubernetesProviderFactoryTestsBase() { var config = new FileConfiguration(); config.GlobalConfiguration.ServiceDiscoveryProvider = new() { Scheme = Uri.UriSchemeHttp, Host = "localhost", Port = 888, Namespace = nameof(KubernetesProviderFactoryTests), Token = TestID, }; var configuration = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddOcelot(config, null, MergeOcelotJson.ToMemory) .Build(); _builder = new ServiceCollection().AddOcelot(configuration); } protected IServiceDiscoveryProvider CreateProvider(string providerType) { var serviceProvider = _builder.Services.BuildServiceProvider(true); var config = GivenServiceProvider(providerType); var route = GivenRoute(); return serviceProvider .GetRequiredService() // returns KubernetesProviderFactory.Get instance .Invoke(serviceProvider, config, route); } protected static ServiceProviderConfiguration GivenServiceProvider(string type) => new() { Type = type, Scheme = string.Empty, Host = string.Empty, Port = 1, Token = string.Empty, ConfigurationKey = string.Empty, PollingInterval = 9_000, }; private static DownstreamRoute GivenRoute([CallerMemberName] string serviceName = nameof(KubernetesProviderFactoryTests)) => new DownstreamRouteBuilder().WithServiceName(serviceName).Build(); } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/ObservableExtensionsTests.cs ================================================ using Microsoft.Reactive.Testing; using Ocelot.Provider.Kubernetes; using System.Reactive.Disposables; using System.Reactive.Linq; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "2168")] [Trait("PR", "2174")] // https://github.com/ThreeMammals/Ocelot/pull/2174 public class ObservableExtensionsTests { private readonly TestScheduler _testScheduler = new(); [Fact] public async Task RetryAfter_ExceptionThrown_RetriesInfiniteWithDelay() { // Arrange var errorsToThrow = Random.Shared.Next(10, 1000); var errorsCounter = 0; var expectedResult = 123; var delaySeconds = TimeSpan.FromSeconds(3); var observable = Observable.Create(observer => { if (errorsCounter < errorsToThrow) { errorsCounter++; throw new Exception("Need to catch and retry"); } observer.OnNext(expectedResult); return Disposable.Empty; }); // Act using var cts = new CancellationTokenSource(); _ = Task.Run(() => { // have to spin in separate thread because it is used after first subscription and stops after first Exception while (!cts.Token.IsCancellationRequested) { _testScheduler.Start(); } }, TestContext.Current.CancellationToken); var result = await observable.RetryAfter(delaySeconds, _testScheduler).FirstAsync(); await cts.CancelAsync(); // Assert result.ShouldBe(expectedResult); errorsCounter.ShouldBe(errorsToThrow); _testScheduler.Clock.ShouldBe(delaySeconds.Ticks * errorsToThrow); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/OcelotBuilderExtensionsTests.cs ================================================ using KubeClient; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Ocelot.DependencyInjection; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.ServiceDiscovery; using System.Reflection; namespace Ocelot.UnitTests.Kubernetes; public class OcelotBuilderExtensionsTests : UnitTest // No Chinese tests now! { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; private IOcelotBuilder _ocelotBuilder; public OcelotBuilderExtensionsTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); } private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment.Setup(e => e.ApplicationName) .Returns(typeof(OcelotBuilderExtensionsTests).GetTypeInfo().Assembly.GetName().Name); return environment.Object; } [Fact] [Trait("Feat", "345")] public void AddKubernetes_NoExceptions_ShouldSetUpKubernetes() { // Arrange var addOcelot = () => _ocelotBuilder = _services.AddOcelot(_configRoot); addOcelot.ShouldNotThrow(); // Act var addKubernetes = () => _ocelotBuilder.AddKubernetes(); // Assert addKubernetes.ShouldNotThrow(); } [Fact] [Trait("Bug", "977")] [Trait("PR", "2180")] public void AddKubernetes_DefaultServices_HappyPath() { // Arrange, Act _ocelotBuilder = _services.AddOcelot(_configRoot).AddKubernetes(); // Assert AssertServices(); } [Fact] [Trait("Feat", "2256")] public void AddKubernetes_NoAction_HappyPath() { // Arrange Action noAction = null; _ocelotBuilder = _services.AddOcelot(_configRoot); // Act _ocelotBuilder.AddKubernetes(noAction); // Assert AssertServices(); Assert>(); // not IOptions } private void AssertServices() { Assert(); // 2180 scenario Assert(); Assert(); Assert(); } private void Assert(ServiceLifetime lifetime = ServiceLifetime.Singleton) where T : class { var descriptor = _services.SingleOrDefault(Of).ShouldNotBeNull(); descriptor.Lifetime.ShouldBe(lifetime); } private static bool Of(ServiceDescriptor descriptor) where T : class => descriptor.ServiceType.Equals(typeof(T)); } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/PollKubeTests.cs ================================================ using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; using System.Collections.Concurrent; using System.Reflection; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "345")] // https://github.com/ThreeMammals/Ocelot/issues/345 public sealed class PollKubeTests : UnitTest, IDisposable { private PollKube _provider; private readonly Mock _factory = new(); private readonly Mock _logger = new(); private readonly Mock _discoveryProvider = new(); const int PollingIntervalMs = 1; public PollKubeTests() { _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); } public void Dispose() { _provider?.Dispose(); } [Fact] public void Dispose_Manually() { var instance = new PollKube(10_000, _factory.Object, _discoveryProvider.Object); instance.Dispose(); } [Fact] [Trait("PR", "772")] // https://github.com/ThreeMammals/Ocelot/pull/772 public void Should_return_service_from_kube() { // Arrange var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); List services = [service]; _discoveryProvider.Setup(x => x.GetAsync()).ReturnsAsync(services); _provider = new PollKube(PollingIntervalMs, _factory.Object, _discoveryProvider.Object); // Act var actual = WhenIGetTheServices(1); // Assert Assert.NotNull(actual); Assert.Equal(1, actual.Count); } private List WhenIGetTheServices(int expected) { List services = null; var result = Wait.For(3_000).Until(() => { try { services = _provider.GetAsync().GetAwaiter().GetResult(); return services.Count == expected; } catch (Exception) { return false; } }); Assert.True(result); return services; } [Fact(Skip = "Require coverage checks")] [Trait("Bug", "2304")] // https://github.com/ThreeMammals/Ocelot/issues/2304 public async Task OnTimerCallbackAsync_AvoidPolling_WhenAlreadyPolling() { // Arrange int pollingInterval = 100; var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); List services = [service]; var slowPolling = Task.Delay(pollingInterval + 50, TestContext.Current.CancellationToken) .ContinueWith(x => services); _discoveryProvider.Setup(x => x.GetAsync()).Returns(slowPolling); _provider = new PollKube(pollingInterval, _factory.Object, _discoveryProvider.Object); // Act var coldRequestTask = _provider.GetAsync(); // calls Poll() due to empty queue var method = _provider.GetType().GetMethod("OnTimerCallbackAsync", BindingFlags.Instance | BindingFlags.NonPublic); method.Invoke(_provider, [new object()]); _discoveryProvider.Verify(x => x.GetAsync(), Times.Once); var actual = await coldRequestTask; _discoveryProvider.Verify(x => x.GetAsync(), Times.AtLeastOnce); // ideally it shoud be called once, but it is called 1 or 2 times. TODO A disposing enhancement? method.Invoke(_provider, [new object()]); _discoveryProvider.Verify(x => x.GetAsync(), Times.AtLeast(2)); } [Fact] [Trait("Bug", "2304")] // https://github.com/ThreeMammals/Ocelot/issues/2304 public async Task GetAsync() { // Arrange int pollingInterval = 100; var service = new Service(string.Empty, new ServiceHostAndPort(string.Empty, 0), string.Empty, string.Empty, new List()); List services = [service]; var slowPolling = Task.Delay(pollingInterval + 50, TestContext.Current.CancellationToken).ContinueWith(x => services); _discoveryProvider.Setup(x => x.GetAsync()).Returns(slowPolling); _provider = new PollKube(pollingInterval, _factory.Object, _discoveryProvider.Object); FieldInfo pollingField = _provider.GetType().GetField("_polling", BindingFlags.Instance | BindingFlags.NonPublic); pollingField.SetValue(_provider, true); FieldInfo queueField = _provider.GetType().GetField("_queue", BindingFlags.Instance | BindingFlags.NonPublic); var queue = queueField.GetValue(_provider) as ConcurrentQueue>; List oldVersion = [service]; queue.Enqueue(oldVersion); // Act var actual = await _provider.GetAsync(); // will NOT call Poll() Assert.Same(oldVersion, actual); _discoveryProvider.Verify(x => x.GetAsync(), Times.Never); // Scenario 2: For services with multiple versions, remove outdated versions and retain only the latest one pollingField.SetValue(_provider, false); List latestVersion = [new Service("", new("h", 123), "", "", default)]; queue.Enqueue(latestVersion); Assert.Equal(2, queue.Count); actual = await _provider.GetAsync(); // will NOT call Poll() Assert.Equal(1, queue.Count); Assert.Same(latestVersion, actual); _discoveryProvider.Verify(x => x.GetAsync(), Times.Never); } } ================================================ FILE: test/Ocelot.UnitTests/Kubernetes/WatchKubeTests.cs ================================================ using KubeClient; using KubeClient.Models; using Microsoft.Extensions.Logging; using Microsoft.Reactive.Testing; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Provider.Kubernetes.Interfaces; using Ocelot.Responses; using Ocelot.Values; using System.Linq.Expressions; using System.Reactive.Linq; namespace Ocelot.UnitTests.Kubernetes; [Trait("Feat", "2168")] [Trait("PR", "2174")] // https://github.com/ThreeMammals/Ocelot/pull/2174 public class WatchKubeTests { private readonly Mock _loggerFactory = new(); private readonly Mock _kubeApiClient = new(); private readonly Mock _endpointClient = new(); private readonly Mock _kubeServiceBuilder = new(); private readonly TestScheduler _testScheduler = new(); private readonly KubeRegistryConfiguration _config = new() { KubeNamespace = "dummy-namespace", KeyOfServiceInK8s = "dummy-service", }; private readonly OcelotLogger _ocLogger; private readonly Mock _logger = new(); private readonly Mock _dataRepository = new(); private readonly Expression>>> _watch; public WatchKubeTests() { _logger.Setup(x => x.IsEnabled(It.IsAny())) .Returns(true); _logger.Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) .Verifiable(); _dataRepository.Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse("123")); _ocLogger = new(_logger.Object, _dataRepository.Object); _loggerFactory.Setup(x => x.CreateLogger()) .Returns(_ocLogger); _kubeApiClient.Setup(x => x.ResourceClient(It.IsAny>())) .Returns(_endpointClient.Object); _kubeServiceBuilder.Setup(x => x.BuildServices(It.IsAny(), It.IsAny())) .Returns((KubeRegistryConfiguration config, EndpointsV1 endpoints) => { return endpoints.Subsets.Select((x, i) => new Service( config.KeyOfServiceInK8s, new ServiceHostAndPort(x.Addresses[i].Hostname, x.Ports[i].Port!.Value), i.ToString(), endpoints.ApiVersion, Enumerable.Empty())); }); _watch = x => x.Watch(It.Is(s => s == _config.KeyOfServiceInK8s), It.IsAny(), It.IsAny()); } [Theory] [InlineData(ResourceEventType.Added, 1)] [InlineData(ResourceEventType.Modified, 1)] [InlineData(ResourceEventType.Bookmark, 1)] [InlineData(ResourceEventType.Error, 0)] [InlineData(ResourceEventType.Deleted, 0)] public async Task GetAsync_EndpointsEventObserved_ServicesReturned(ResourceEventType eventType, int expectedServicesCount) { // Arrange var eventDelay = TimeSpan.FromMilliseconds(Random.Shared.Next(1, (WatchKube.FirstResultsFetchingTimeoutSeconds * 1000) - 1)); var endpointsObservable = CreateOneEvent(eventType).ToObservable().Delay(eventDelay, _testScheduler); _endpointClient.Setup(_watch).Returns(endpointsObservable); // Act var watchKube = CreateWatchKube(); _testScheduler.AdvanceBy(eventDelay.Ticks); var services = await watchKube.GetAsync(); // Assert services.Count.ShouldBe(expectedServicesCount); } [Fact] public async Task GetAsync_NoEventsAfterTimeout_EmptyServicesReturned() { // Arrange _endpointClient.Setup(_watch) .Returns(Observable.Create>(_ => Mock.Of())); // Act var watchKube = CreateWatchKube(); _testScheduler.Start(); var services = await watchKube.GetAsync(); // Assert services.ShouldBeEmpty(); _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FirstResultsFetchingTimeoutSeconds).Ticks); _logger.Verify( x => x.Log(LogLevel.Warning, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once()); } [Fact] public async Task GetAsync_WatchFailed_RetriedAfterDelay() { // Arrange var subscriptionAttempts = 0; var observable = Observable.Create>(observer => { if (subscriptionAttempts == 0) { observer.OnError(new HttpRequestException("Error occured in first watch request")); } else { observer.OnNext(CreateOneEvent(ResourceEventType.Added).First()); } subscriptionAttempts++; return Mock.Of(); }); _endpointClient.Setup(_watch).Returns(observable); // Act var watchKube = CreateWatchKube(); _testScheduler.Start(); var services = await watchKube.GetAsync(); // Assert services.Count.ShouldBe(1); subscriptionAttempts.ShouldBe(2); _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FailedSubscriptionRetrySeconds).Ticks); _logger.Verify( x => x.Log(LogLevel.Error, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once()); } [Fact] public async Task Dispose_OnSubscriptionCancellation_LogsInformation() { // Arrange var observable = Observable.Create>(observer => { observer.OnCompleted(); return Mock.Of(); }); _endpointClient.Setup(_watch).Returns(observable); // Act var watchKube = CreateWatchKube(); _testScheduler.Start(); var services = await watchKube.GetAsync(); watchKube.Dispose(); // Assert services.ShouldBeEmpty(); _testScheduler.Clock.ShouldBe(TimeSpan.FromSeconds(WatchKube.FirstResultsFetchingTimeoutSeconds).Ticks); _logger.Verify( x => x.Log(LogLevel.Information, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once()); } [Theory] [InlineData(false)] [InlineData(true)] public async Task GetAsync_EndpointsEventObserved_NoServices(bool branch1) { // Arrange var eventDelay = TimeSpan.FromMilliseconds(Random.Shared.Next(1, (WatchKube.FirstResultsFetchingTimeoutSeconds * 1000) - 1)); EndpointsV1 endpoints = null; if (branch1) { endpoints = new EndpointsV1 { Kind = "endpoint", ApiVersion = "1.0", Metadata = new ObjectMetaV1 { Name = _config.KeyOfServiceInK8s, Namespace = _config.KubeNamespace, }, }; endpoints.Subsets.Clear(); } var resourceEvent = new ResourceEventV1 { EventType = ResourceEventType.Bookmark, Resource = endpoints, }; var events = new ResourceEventV1[] { resourceEvent }; var endpointsObservable = events.ToObservable().Delay(eventDelay, _testScheduler); _endpointClient.Setup(_watch).Returns(endpointsObservable); // Act var watchKube = CreateWatchKube(); _testScheduler.AdvanceBy(eventDelay.Ticks); var services = await watchKube.GetAsync(); // Assert services.ShouldBeEmpty(); } [Theory] [InlineData(-1, 1)] [InlineData(0, 1)] [InlineData(1, 1)] [InlineData(3, 3)] public void StaticProperties_Setter_ShouldBeGreaterThanOrEqualToOne(int value, int expected) { WatchKube.FailedSubscriptionRetrySeconds = value; Assert.Equal(expected, WatchKube.FailedSubscriptionRetrySeconds); WatchKube.FirstResultsFetchingTimeoutSeconds = value; Assert.Equal(expected, WatchKube.FirstResultsFetchingTimeoutSeconds); } private WatchKube CreateWatchKube() => new(_config, _loggerFactory.Object, _kubeApiClient.Object, _kubeServiceBuilder.Object, _testScheduler); private IResourceEventV1[] CreateOneEvent(ResourceEventType eventType) { var resourceEvent = new ResourceEventV1 { EventType = eventType, Resource = CreateEndpoints(), }; return [resourceEvent]; } private EndpointsV1 CreateEndpoints() { var endpoints = new EndpointsV1 { Kind = "endpoint", ApiVersion = "1.0", Metadata = new ObjectMetaV1 { Name = _config.KeyOfServiceInK8s, Namespace = _config.KubeNamespace, }, }; var subset = new EndpointSubsetV1(); subset.Addresses.Add(new EndpointAddressV1 { Ip = "127.0.0.1", Hostname = "localhost" }); subset.Ports.Add(new EndpointPortV1 { Port = 80 }); endpoints.Subsets.Add(subset); return endpoints; } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.LoadBalancer; public class CookieStickySessionsCreatorTests : UnitTest { private readonly CookieStickySessionsCreator _creator; private readonly Mock _serviceProvider; public CookieStickySessionsCreatorTests() { _creator = new(); _serviceProvider = new(); } [Fact] public void Should_return_instance_of_expected_load_balancer_type() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions("myType", "myKey", 1000)) .Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.ShouldBeOfType(); } [Fact] public void Should_return_expected_name() { // Arrange, Act, Assert _creator.Type.ShouldBe(nameof(CookieStickySessions)); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/CookieStickySessionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.Builder; using Ocelot.Infrastructure; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Middleware; using Ocelot.Responses; using Ocelot.UnitTests.Responder; using Ocelot.Values; using System.Collections; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.LoadBalancer; [Trait("Feat", "336")] public sealed class CookieStickySessionsTests : UnitTest { private readonly CookieStickySessions _stickySessions; private readonly Mock _loadBalancer; private readonly int _defaultExpiryInMs; private readonly FakeBus _bus; private readonly DefaultHttpContext _httpContext; public CookieStickySessionsTests() { _httpContext = new DefaultHttpContext(); _bus = new FakeBus(); _loadBalancer = new Mock(); _defaultExpiryInMs = 0; _stickySessions = new CookieStickySessions(_loadBalancer.Object, "sessionid", _defaultExpiryInMs, _bus); } private void Arrange([CallerMemberName] string serviceName = null) { var route = new DownstreamRouteBuilder() .WithLoadBalancerKey(serviceName) .Build(); _httpContext.Items.UpsertDownstreamRoute(route); } [Fact] public async Task Should_return_host_and_port() { Arrange(); GivenTheLoadBalancerReturns(); GivenTheDownstreamRequestHasSessionId("321"); // Act var result = await _stickySessions.LeaseAsync(_httpContext); // Assert result.Data.ShouldNotBeNull(); } [Fact] public async Task Should_return_same_host_and_port() { Arrange(); GivenTheLoadBalancerReturnsSequence(); GivenTheDownstreamRequestHasSessionId("321"); // Act var firstHostAndPort = await _stickySessions.LeaseAsync(_httpContext); var secondHostAndPort = await _stickySessions.LeaseAsync(_httpContext); // Assert firstHostAndPort.Data.DownstreamHost.ShouldBe(secondHostAndPort.Data.DownstreamHost); firstHostAndPort.Data.DownstreamPort.ShouldBe(secondHostAndPort.Data.DownstreamPort); _bus.Messages.Count.ShouldBe(2); } [Fact] public async Task Should_return_different_host_and_port_if_load_balancer_does() { Arrange(); GivenTheLoadBalancerReturnsSequence(); // When I Make Two Requets With Different Session Values var contextOne = new DefaultHttpContext(); var cookiesOne = new FakeCookies(); cookiesOne.AddCookie("sessionid", "321"); contextOne.Request.Cookies = cookiesOne; var route = new DownstreamRouteBuilder() .WithLoadBalancerKey(nameof(Should_return_different_host_and_port_if_load_balancer_does)) .Build(); contextOne.Items.UpsertDownstreamRoute(route); var contextTwo = new DefaultHttpContext(); var cookiesTwo = new FakeCookies(); cookiesTwo.AddCookie("sessionid", "123"); contextTwo.Request.Cookies = cookiesTwo; contextTwo.Items.UpsertDownstreamRoute(route); // Act var firstHostAndPort = await _stickySessions.LeaseAsync(contextOne); var secondHostAndPort = await _stickySessions.LeaseAsync(contextTwo); // Assert firstHostAndPort.Data.DownstreamHost.ShouldBe("one"); firstHostAndPort.Data.DownstreamPort.ShouldBe(80); secondHostAndPort.Data.DownstreamHost.ShouldBe("two"); secondHostAndPort.Data.DownstreamPort.ShouldBe(80); } [Fact] public async Task Should_return_error() { Arrange(); _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new ErrorResponse(new AnyError())); // Act var result = await _stickySessions.LeaseAsync(_httpContext); // Assert result.IsError.ShouldBeTrue(); } [Fact] public async Task Should_expire_sticky_session() { Arrange(); GivenTheLoadBalancerReturns(); GivenTheDownstreamRequestHasSessionId("321"); GivenIHackAMessageInWithAPastExpiry(); // Act var result = await _stickySessions.LeaseAsync(_httpContext); _bus.Process(); // Assert _loadBalancer.Verify(x => x.Release(It.IsAny()), Times.Once); } [Fact] public void Should_release() { // Arrange, Act, Assert _stickySessions.Release(new ServiceHostAndPort(string.Empty, 0)); } [Fact] public void Type_Is_CookieStickySessions() { // Arrange, Act, Assert Assert.Equal("CookieStickySessions", _stickySessions.Type); } private void GivenIHackAMessageInWithAPastExpiry() { var hostAndPort = new ServiceHostAndPort("999", 999); _bus.Publish(new StickySession(hostAndPort, DateTime.UtcNow.AddDays(-1), "321"), 0); } private void GivenTheLoadBalancerReturnsSequence() { _loadBalancer .SetupSequence(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("one", 80))) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("two", 80))); } private void GivenTheDownstreamRequestHasSessionId(string value) { var cookies = new FakeCookies(); cookies.AddCookie("sessionid", value); _httpContext.Request.Cookies = cookies; } private void GivenTheLoadBalancerReturns() { _loadBalancer .Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort(string.Empty, 80))); } } internal class FakeCookies : IRequestCookieCollection { private readonly Dictionary _cookies = new(); public string this[string key] => _cookies[key]; public int Count => _cookies.Count; public ICollection Keys => _cookies.Keys; public void AddCookie(string key, string value) => _cookies[key] = value; public bool ContainsKey(string key) => _cookies.ContainsKey(key); public IEnumerator> GetEnumerator() => _cookies.GetEnumerator(); public bool TryGetValue(string key, out string value) => _cookies.TryGetValue(key, out value); IEnumerator IEnumerable.GetEnumerator() => _cookies.GetEnumerator(); } internal class FakeBus : IBus { public FakeBus() { Messages = new List(); Subscriptions = new List>(); } public List Messages { get; } public List> Subscriptions { get; } public void Subscribe(Action action) => Subscriptions.Add(action); public void Publish(T message, int delay) => Messages.Add(message); public void Process() { foreach (var message in Messages) { foreach (var subscription in Subscriptions) { subscription(message); } } } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/DelegateInvokingLoadBalancerCreatorTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.Creators; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class DelegateInvokingLoadBalancerCreatorTests : UnitTest { private DelegateInvokingLoadBalancerCreator _creator; private Func _creatorFunc; private readonly Mock _serviceProvider; public DelegateInvokingLoadBalancerCreatorTests() { _creatorFunc = (route, serviceDiscoveryProvider) => new FakeLoadBalancer(route, serviceDiscoveryProvider); _creator = new DelegateInvokingLoadBalancerCreator(_creatorFunc); _serviceProvider = new Mock(); } [Fact] public void Should_return_expected_name() { // Arrange var route = new DownstreamRouteBuilder().Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.Type.ShouldBe(nameof(FakeLoadBalancer)); } [Fact] public void Should_return_result_of_specified_creator_func() { // Arrange var route = new DownstreamRouteBuilder().Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.ShouldBeOfType(); } [Fact] public void Should_return_error() { // Arrange var route = new DownstreamRouteBuilder().Build(); _creatorFunc = (route, serviceDiscoveryProvider) => throw new Exception(); _creator = new DelegateInvokingLoadBalancerCreator(_creatorFunc); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.IsError.ShouldBeTrue(); } [Fact] public void Type() { // Arrange, Act, Assert Assert.Equal(nameof(FakeLoadBalancer), _creator.Type); } private class FakeLoadBalancer : ILoadBalancer { public FakeLoadBalancer(DownstreamRoute downstreamRoute, IServiceDiscoveryProvider serviceDiscoveryProvider) { DownstreamRoute = downstreamRoute; ServiceDiscoveryProvider = serviceDiscoveryProvider; } public DownstreamRoute DownstreamRoute { get; } public IServiceDiscoveryProvider ServiceDiscoveryProvider { get; } public string Type => nameof(FakeLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LeaseEventArgsTests.cs ================================================ using Ocelot.LoadBalancer; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class LeaseEventArgsTests { [Fact] public void Ctor() { // Arrange ServiceHostAndPort host = new("host", 123); Lease lease = new(host, 3); Service service = new("s", new("h", 123), "", "", []); // Act LeaseEventArgs args = new(lease, service, 3); // Assert Assert.Equal(lease, args.Lease); Assert.Equal(service, args.Service); Assert.Equal(3, args.ServiceIndex); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LeaseTests.cs ================================================ using Ocelot.LoadBalancer; using Ocelot.Values; using Steeltoe.Connector; namespace Ocelot.UnitTests.LoadBalancer; public class LeaseTests { [Fact] public void Ctor() { // Arrange, Act Lease l = new(); // Assert Assert.Null(l.HostAndPort); Assert.Equal(0, l.Connections); } [Fact] public void Ctor_Lease() { // Arrange ServiceHostAndPort host = new("host", 123); Lease from = new(host, 3); // Act Lease actual = new(from); // Assert Assert.Equivalent(from, actual); Assert.Equivalent(3, actual.Connections); } [Fact] public void Ctor_ServiceHostAndPort() { // Arrange ServiceHostAndPort hostAndPort = new("host", 123); // Act Lease actual = new(hostAndPort); // Assert Assert.Equivalent(hostAndPort, actual.HostAndPort); Assert.Equal(hostAndPort, actual.HostAndPort); Assert.Equal(0, actual.Connections); } [Fact] public void Ctor_Init() { // Arrange ServiceHostAndPort hostAndPort = new("host", 123); int connections = 3; // Act Lease actual = new(hostAndPort, connections); // Assert Assert.Equivalent(hostAndPort, actual.HostAndPort); Assert.Equal(hostAndPort, actual.HostAndPort); Assert.Equal(3, actual.Connections); } [Fact] public void Null() { // Arrange, Act Lease actual = Lease.Null; // Assert Assert.Null(actual.HostAndPort); Assert.Equal(0, actual.Connections); } [Fact] public void ToString_HostPlusConnections() { // Arrange Lease l = new(new("host", 333, "ws"), 4); // Act var actual = l.ToString(); // Assert Assert.NotNull(actual); Assert.Equal("(ws:host:333+4)", actual); } [Fact] public void Equals_object() { // Arrange, Act, Assert Lease l = Lease.Null; var boxed = (object)l; bool equality = l.Equals(boxed); Assert.True(equality); // Arrange, Act, Assert l = new(new("host", 333, "ws"), 4); boxed = (object)l; equality = Lease.Null.Equals(boxed); Assert.False(equality); // Arrange, Act, Assert string s = "not Lease"; boxed = (object)s; equality = l.Equals(boxed); Assert.False(equality); } [Fact] public void Equals_Lease() { // Arrange, Act, Assert : false Lease l = new(new("host", 333, "ws"), 4); Lease other = Lease.Null; bool equality = l.Equals(other); Assert.False(equality); // Arrange, Act, Assert : true equality = Lease.Null.Equals(other); Assert.True(equality); } [Fact] public void Op_Inequality_Lease_Lease() { // Arrange, Act, Assert : true Lease x = new(new("host", 333, "ws"), 4); Lease y = Lease.Null; bool equality = x != y; Assert.True(equality); // Arrange, Act, Assert : false equality = Lease.Null != y; Assert.False(equality); } [Fact] public void Op_Inequality_ServiceHostAndPort_Lease() { // Arrange, Act, Assert : false ServiceHostAndPort h = new("host", 333, "ws"); Lease l = new(h, 1); bool equality = h != l; Assert.False(equality); // Arrange, Act, Assert : true equality = h != Lease.Null; Assert.True(equality); } [Fact] public void Op_Inequality_Lease_ServiceHostAndPort() { // Arrange, Act, Assert : false ServiceHostAndPort h = new("host", 333, "ws"); Lease l = new(h, 1); bool equality = l != h; Assert.False(equality); // Arrange, Act, Assert : true equality = Lease.Null != h; Assert.True(equality); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LeastConnectionCreatorTests.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; using System.Reflection; namespace Ocelot.UnitTests.LoadBalancer; public class LeastConnectionCreatorTests : UnitTest { private readonly LeastConnectionCreator _creator; private readonly Mock _serviceProvider; public LeastConnectionCreatorTests() { _creator = new(); _serviceProvider = new(); } [Theory] [InlineData(false)] [InlineData(true)] public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName) { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName(isNullServiceName ? null : "myService") .WithLoadBalancerKey("key") .Build(); // Act var response = _creator.Create(route, _serviceProvider.Object); // Assert response.Data.ShouldBeOfType(); var balancer = response.Data as LeastConnection; var field = balancer.GetType().GetField("_serviceName", BindingFlags.Instance | BindingFlags.NonPublic); var serviceName = field.GetValue(balancer) as string; serviceName.ShouldBe(isNullServiceName ? "key" : "myService"); } [Fact] public void Should_return_expected_name() { // Arrange, Act, Assert _creator.Type.ShouldBe(nameof(LeastConnection)); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LeastConnectionTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Balancers; using Ocelot.Values; using Ocelot.LoadBalancer.Interfaces; using Ocelot.LoadBalancer; namespace Ocelot.UnitTests.LoadBalancer; public class LeastConnectionTests : UnitTest { private LeastConnection _leastConnection; private readonly Random _random; private readonly DefaultHttpContext _httpContext; public LeastConnectionTests() { _httpContext = new(); _random = new(); } [Fact] public async Task Should_be_able_to_lease_and_release_concurrently() { const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); var tasks = new Task[100]; for (var i = 0; i < tasks.Length; i++) { tasks[i] = LeaseDelayAndRelease(); } await Task.WhenAll(tasks); } [Fact] public async Task Should_handle_service_returning_to_available() { const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); var hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext); hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); var hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext); hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), }; hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext); hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext); hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.1"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; hostAndPortOne = await _leastConnection.LeaseAsync(_httpContext); hostAndPortOne.Data.DownstreamHost.ShouldBe("127.0.0.1"); hostAndPortTwo = await _leastConnection.LeaseAsync(_httpContext); hostAndPortTwo.Data.DownstreamHost.ShouldBe("127.0.0.2"); _leastConnection.Release(hostAndPortOne.Data); _leastConnection.Release(hostAndPortTwo.Data); } private async Task LeaseDelayAndRelease() { var hostAndPort = await _leastConnection.LeaseAsync(_httpContext); await Task.Delay(_random.Next(1, 100)); _leastConnection.Release(hostAndPort.Data); } [Fact] public async Task Should_get_next_url() { // Arrange const string ServiceName = "products"; var hostAndPort = new ServiceHostAndPort("localhost", 80); var availableServices = new List { new(ServiceName, hostAndPort, string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); // Act var result = await _leastConnection.LeaseAsync(_httpContext); // Assert result.Data.DownstreamHost.ShouldBe(hostAndPort.DownstreamHost); result.Data.DownstreamPort.ShouldBe(hostAndPort.DownstreamPort); } [Fact] public async Task Should_serve_from_service_with_least_connections() { const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.3", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); var response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[2].HostAndPort.DownstreamHost); } [Fact] public async Task Should_build_connections_per_service() { const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); var response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); } [Fact] public async Task Should_release_connection() { const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), new(ServiceName, new ServiceHostAndPort("127.0.0.2", 80), string.Empty, string.Empty, Array.Empty()), }; _leastConnection = new LeastConnection(() => Task.FromResult(availableServices), ServiceName); var response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[0].HostAndPort.DownstreamHost); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); //release this so 2 should have 1 connection and we should get 2 back as our next host and port _leastConnection.Release(availableServices[1].HostAndPort); response = await _leastConnection.LeaseAsync(_httpContext); response.Data.DownstreamHost.ShouldBe(availableServices[1].HostAndPort.DownstreamHost); } [Fact] public async Task Should_return_error_if_services_are_null() { // Arrange const string ServiceName = "products"; var hostAndPort = new ServiceHostAndPort("localhost", 80); _leastConnection = new LeastConnection(() => Task.FromResult((List)null), ServiceName); // Act var result = await _leastConnection.LeaseAsync(_httpContext); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].ShouldBeOfType(); } [Fact] public async Task Should_return_error_if_services_are_empty() { // Arrange const string ServiceName = "products"; _leastConnection = new LeastConnection(() => Task.FromResult(new List()), ServiceName); // Act var result = await _leastConnection.LeaseAsync(_httpContext); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].ShouldBeOfType(); } [Fact] public async Task OnLeased() { // Arrange const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), }; var leastConnection = new TestLeastConnection(() => Task.FromResult(availableServices), ServiceName); // Act var result = await leastConnection.LeaseAsync(_httpContext); // Assert leastConnection.Events.ShouldNotBeEmpty(); var args = leastConnection.Events[0].ShouldNotBeNull(); args.Service.Name.ShouldBe(ServiceName); } } internal sealed class TestLeastConnection : LeastConnection, ILoadBalancer { public readonly List Events = new(); public TestLeastConnection(Func>> services, string serviceName) : base(services, serviceName) => Leased += Me_Leased; private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LoadBalancerFactoryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Infrastructure.RequestData; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerFactoryTests : UnitTest { private readonly LoadBalancerFactory _factory; private readonly Mock _serviceProviderFactory; private readonly IEnumerable _loadBalancerCreators; private readonly Mock _serviceProvider; public LoadBalancerFactoryTests() { _serviceProviderFactory = new Mock(); _serviceProvider = new Mock(); _loadBalancerCreators = new ILoadBalancerCreator[] { new FakeLoadBalancerCreator(), new FakeLoadBalancerCreator(), new FakeLoadBalancerCreator(nameof(NoLoadBalancer)), new BrokenLoadBalancerCreator(), }; _factory = new LoadBalancerFactory(_serviceProviderFactory.Object, _loadBalancerCreators); } [Fact] public void Should_return_no_load_balancer_by_default() { // Arrange var route = new DownstreamRouteBuilder() .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); // Act var result = _factory.Get(route, config); // Assert result.Data.ShouldBeOfType(); } [Fact] public void Should_return_matching_load_balancer() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerTwo), string.Empty, 0)) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); // Act var result = _factory.Get(route, config); // Assert result.Data.ShouldBeOfType(); } [Fact] public void Should_return_error_response_if_cannot_find_load_balancer_creator() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions("DoesntExistLoadBalancer", string.Empty, 0)) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); // Act var result = _factory.Get(route, config); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].Message.ShouldBe("Could not find load balancer creator for Type: DoesntExistLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name."); } [Fact] public void Should_return_error_response_if_creator_errors() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(BrokenLoadBalancer), string.Empty, 0)) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); // Act var result = _factory.Get(route, config); // Assert result.IsError.ShouldBeTrue(); } [Fact] public void Should_call_service_provider() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0)) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryReturns(); // Act var result = _factory.Get(route, config); // Assert ThenTheServiceProviderIsCalledCorrectly(); } [Fact] public void Should_return_error_response_when_call_to_service_provider_fails() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancerOne), string.Empty, 0)) .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var config = new ServiceProviderConfigurationBuilder().Build(); GivenTheServiceProviderFactoryFails(); // Act var result = _factory.Get(route, config); // Assert result.IsError.ShouldBeTrue(); } private void GivenTheServiceProviderFactoryReturns() { _serviceProviderFactory .Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(new OkResponse(_serviceProvider.Object)); } private void GivenTheServiceProviderFactoryFails() { _serviceProviderFactory .Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(new ErrorResponse(new CannotFindDataError("For tests"))); } private void ThenTheServiceProviderIsCalledCorrectly() { _serviceProviderFactory .Verify(x => x.Get(It.IsAny(), It.IsAny()), Times.Once); } private class FakeLoadBalancerCreator : ILoadBalancerCreator where T : ILoadBalancer, new() { public FakeLoadBalancerCreator() => Type = typeof(T).Name; public FakeLoadBalancerCreator(string type) => Type = type; public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) => new OkResponse(new T()); public string Type { get; } } private class BrokenLoadBalancerCreator : ILoadBalancerCreator where T : ILoadBalancer, new() { public BrokenLoadBalancerCreator() => Type = typeof(T).Name; public Response Create(DownstreamRoute route, IServiceDiscoveryProvider serviceProvider) => new ErrorResponse(new InvokingLoadBalancerCreatorError(new Exception())); public string Type { get; } } private class FakeLoadBalancerOne : ILoadBalancer { public string Type => nameof(FakeLoadBalancerOne); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } private class FakeLoadBalancerTwo : ILoadBalancer { public string Type => nameof(FakeLoadBalancerTwo); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } private class FakeNoLoadBalancer : ILoadBalancer { public string Type => nameof(FakeNoLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } private class BrokenLoadBalancer : ILoadBalancer { public string Type => nameof(BrokenLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LoadBalancerHouseTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerHouseTests : UnitTest { private readonly LoadBalancerHouse _house; private readonly Mock _factory; private readonly ServiceProviderConfiguration _serviceProviderConfig; public LoadBalancerHouseTests() { _factory = new Mock(); _house = new LoadBalancerHouse(_factory.Object); _serviceProviderConfig = new() { Type = "myType", Scheme = "myScheme", Host = "myHost", Port = 123, ConfigurationKey = "configKey", }; } [Fact] public void Should_store_load_balancer_on_first_request() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerKey("test") .Build(); var loadBalancer = new FakeLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); // Act var result = _house.Get(route, _serviceProviderConfig); // Assert: Then It Is Added result.IsError.ShouldBe(false); result.ShouldBeOfType>(); result.Data.ShouldBe(loadBalancer); _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once); } [Fact] public void Should_not_store_load_balancer_on_second_request() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); var loadBalancer = new FakeLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); // Act var result = _house.Get(route, _serviceProviderConfig); // Assert result.Data.ShouldBe(loadBalancer); _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once); } [Fact] public void Should_store_load_balancers_by_key() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); var route2 = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeRoundRobinLoadBalancer), string.Empty, 0)) .WithLoadBalancerKey("testtwo") .Build(); var loadBalancer = new FakeLoadBalancer(); var loadBalancer2 = new FakeRoundRobinLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); _factory.Setup(x => x.Get(route2, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer2)); // Act, Assert var result = _house.Get(route, _serviceProviderConfig); result.Data.ShouldBeOfType(); // Act, Assert result = _house.Get(route2, _serviceProviderConfig); result.Data.ShouldBeOfType(); } [Fact] public void Should_return_error_if_exception() { // Arrange var route = new DownstreamRouteBuilder().Build(); // Act var result = _house.Get(route, _serviceProviderConfig); // Assert result.IsError.ShouldBeTrue(); result.Errors[0].ShouldBeOfType(); } [Fact] public void Should_get_new_load_balancer_if_route_load_balancer_has_changed() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(FakeLoadBalancer), string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); var route2 = new DownstreamRouteBuilder() .WithLoadBalancerOptions(new LoadBalancerOptions(nameof(LeastConnection), string.Empty, 0)) .WithLoadBalancerKey("test") .Build(); var loadBalancer = new FakeLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); // Act, Assert var result = _house.Get(route, _serviceProviderConfig); result.Data.ShouldBeOfType(); _factory.Setup(x => x.Get(route2, _serviceProviderConfig)).Returns(new OkResponse(new LeastConnection(null, null))); // Act, Assert result = _house.Get(route2, _serviceProviderConfig); result.Data.ShouldBeOfType(); } [Fact] public void GetResponse_IsError() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerKey("test") .Build(); var loadBalancer = new FakeLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)) .Returns(new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: FakeLoadBalancer, please check your config specified the correct load balancer and that you have registered a class with the same name."))); // Act var result = _house.Get(route, _serviceProviderConfig); // Assert result.IsError.ShouldBeTrue(); result.ShouldBeOfType>(); result.Data.ShouldBeNull(); _factory.Verify(x => x.Get(route, _serviceProviderConfig), Times.Once); result.Errors.Single().ShouldBeOfType(); } [Fact] public void TypesMismatched_ShouldReturnError() { // Arrange var route = new DownstreamRouteBuilder() .WithLoadBalancerKey("test") .Build(); var loadBalancer = new FakeLoadBalancer(); _factory.Setup(x => x.Get(route, _serviceProviderConfig)).Returns(new OkResponse(loadBalancer)); // Other route has the same LoadBalancerKey but types are different var route2 = new DownstreamRouteBuilder() .WithLoadBalancerKey(route.LoadBalancerKey) .WithLoadBalancerOptions(new() { Type = "bla-bla" }) .Build(); _factory.Setup(x => x.Get(route2, _serviceProviderConfig)) .Returns(new ErrorResponse(new CouldNotFindLoadBalancerCreatorError($"Could not find load balancer creator for Type: {route2.LoadBalancerOptions.Type}, please check your config specified the correct load balancer and that you have registered a class with the same name."))); // Act var result = _house.Get(route2, _serviceProviderConfig); // Assert: Then It Is Added result.IsError.ShouldBeTrue(); result.ShouldBeOfType>(); result.Data.ShouldBeNull(); _factory.Verify(x => x.Get(route2, _serviceProviderConfig), Times.Once); result.Errors.Single().ShouldBeOfType() .Message.ShouldBe("Could not find load balancer creator for Type: bla-bla, please check your config specified the correct load balancer and that you have registered a class with the same name."); } private class FakeLoadBalancer : ILoadBalancer { public string Type => nameof(FakeLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } private class FakeRoundRobinLoadBalancer : ILoadBalancer { public string Type => nameof(FakeRoundRobinLoadBalancer); public Task> LeaseAsync(HttpContext httpContext) => throw new NotImplementedException(); public void Release(ServiceHostAndPort hostAndPort) => throw new NotImplementedException(); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LoadBalancerMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Errors; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerMiddlewareTests : UnitTest { private readonly Mock _loadBalancerHouse; private readonly Mock _loadBalancer; private ServiceHostAndPort _hostAndPort; private ErrorResponse _getLoadBalancerHouseError; private ErrorResponse _getHostAndPortError; private readonly HttpRequestMessage _downstreamRequest; private readonly Mock _loggerFactory; private readonly Mock _logger; private LoadBalancingMiddleware _middleware; private RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public LoadBalancerMiddlewareTests() { _httpContext = new DefaultHttpContext(); _loadBalancer = new Mock(); _loadBalancerHouse = new Mock(); _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com/"); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _loadBalancerHouse.Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(new OkResponse(_loadBalancer.Object)); } private void Arrange() { var downstreamRoute = new DownstreamRouteBuilder() .WithUpstreamHttpMethod([HttpMethods.Get]) .Build(); var serviceProviderConfig = new ServiceProviderConfigurationBuilder() .Build(); GivenTheDownStreamUrlIs("http://my.url/abc?q=123"); GivenTheConfigurationIs(serviceProviderConfig); GivenTheDownStreamRouteIs(downstreamRoute, new List()); } [Fact] public async Task Should_call_scoped_data_repository_correctly() { Arrange(); // Arrange: Given The Load Balancer Returns _hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new OkResponse(_hostAndPort)); // Act _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.DownstreamRequest().ToHttpRequestMessage().RequestUri.OriginalString.ShouldBe("http://127.0.0.1:80/abc?q=123"); } [Fact] public async Task Should_set_pipeline_error_if_cannot_get_load_balancer() { Arrange(); // Arrange: Given The Load Balancer House Returns An Error _getLoadBalancerHouseError = new ErrorResponse(new List { new UnableToFindLoadBalancerError("unabe to find load balancer for bah"), }); _loadBalancerHouse.Setup(x => x.Get(It.IsAny(), It.IsAny())) .Returns(_getLoadBalancerHouseError); // Act _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); _httpContext.Items.Errors().ShouldBe(_getLoadBalancerHouseError.Errors); } [Fact] public async Task Should_set_pipeline_error_if_cannot_get_least() { Arrange(); // Arrange: Given The Load Balancer Returns An Error _getHostAndPortError = new ErrorResponse(new List { new ServicesAreNullError("services were null for bah") }); _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(_getHostAndPortError); // Act _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); _httpContext.Items.Errors().ShouldBe(_getHostAndPortError.Errors); } [Fact] public async Task Should_set_scheme() { Arrange(); // Arrange: Given The Load Balancer Returns Ok _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("abc", 123, "https"))); // Act _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.DownstreamRequest().Host.ShouldBeEquivalentTo("abc"); _httpContext.Items.DownstreamRequest().Port.ShouldBeEquivalentTo(123); _httpContext.Items.DownstreamRequest().Scheme.ShouldBeEquivalentTo("https"); } [Fact] public async Task ShouldNot_LogDebug_WhenNextMiddlewareThrownException() { Arrange(); _next = (context) => throw new NotImplementedException("NextMiddleware"); // Arrange: Given The Load Balancer Returns Ok _loadBalancer.Setup(x => x.LeaseAsync(It.IsAny())) .ReturnsAsync(new OkResponse(new ServiceHostAndPort("abc", 123, "https"))); _logger.Setup(x => x.LogDebug(It.IsAny())).Verifiable(); // Act _middleware = new LoadBalancingMiddleware(_next, _loggerFactory.Object, _loadBalancerHouse.Object); var action = () => _middleware.Invoke(_httpContext); var ex = await action.ShouldThrowAsync(); ex.Message.ShouldBe("NextMiddleware"); _logger.Verify(x => x.LogDebug(It.IsAny()), Times.Never); } private void GivenTheConfigurationIs(ServiceProviderConfiguration config) { var configuration = new InternalConfiguration() { ServiceProviderConfiguration = config, }; _httpContext.Items.SetIInternalConfiguration(configuration); } private void GivenTheDownStreamUrlIs(string downstreamUrl) { _downstreamRequest.RequestUri = new Uri(downstreamUrl); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest)); } private void GivenTheDownStreamRouteIs(DownstreamRoute downstreamRoute, List placeholder) { _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(placeholder); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.LoadBalancer.Balancers; using System.Reflection; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerOptionsCreatorTests : UnitTest { private readonly LoadBalancerOptionsCreator _creator = new(); [Theory] [InlineData(false)] [InlineData(true)] public void Should_create(bool isNull) { // Arrange var options = isNull ? null : new FileLoadBalancerOptions { Type = "test", Key = "west", Expiry = 1, }; // Act var result = _creator.Create(options); // Assert result.Type.ShouldBe(isNull ? "NoLoadBalancer" : "test"); result.Key.ShouldBe(isNull ? null : "west"); result.ExpiryInMs.ShouldBe(isNull ? 0 : 1); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void Create_FromRoute_NullChecks() { // Arrange, Act, Assert FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void Create_FromRoute() { // Arrange FileRoute route = new() { LoadBalancerOptions = new() { Key = "route", }, }; FileGlobalConfiguration globalConfiguration = new() { LoadBalancerOptions = new("global"), }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.Equal("global", actual.Type); Assert.Equal("route", actual.Key); Assert.Equal(0, actual.ExpiryInMs); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void Create_FromDynamicRoute_NullChecks() { // Arrange, Act, Assert FileDynamicRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void Create_FromDynamicRoute() { // Arrange FileDynamicRoute route = new() { LoadBalancerOptions = new() { Key = "route", }, }; FileGlobalConfiguration globalConfiguration = new() { LoadBalancerOptions = new("global"), }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.Equal("global", actual.Type); Assert.Equal("route", actual.Key); Assert.Equal(0, actual.ExpiryInMs); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void CreateProtected_NullCheck() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); IRouteGrouping grouping = null; FileLoadBalancerOptions options = null; FileGlobalLoadBalancerOptions globalOptions = null; // Act var wrapper = Assert.Throws( () => method.Invoke(_creator, [grouping, options, globalOptions])); // Assert Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(grouping), actual.ParamName); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void CreateProtected() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new() { Key = "r1" }; FileLoadBalancerOptions options = null; FileGlobalLoadBalancerOptions globalOptions = new() { RouteKeys = null, Type = "global", Key = "global", Expiry = 1, }; // Act, Assert var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal("global", actual.Type); Assert.Equal("global", actual.Key); Assert.Equal(1, actual.ExpiryInMs); // Arrange 2 options = new() { Type = "route", Key = "route", Expiry = 3, }; globalOptions.RouteKeys = ["?"]; // Act, Assert 2 actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal("route", actual.Type); Assert.Equal("route", actual.Key); Assert.Equal(3, actual.ExpiryInMs); globalOptions.RouteKeys = ["r1"]; actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal("route", actual.Type); Assert.Equal("route", actual.Key); Assert.Equal(3, actual.ExpiryInMs); globalOptions = null; actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal("route", actual.Type); Assert.Equal("route", actual.Key); Assert.Equal(3, actual.ExpiryInMs); // Arrange 3 options.Key = null; globalOptions = new() { RouteKeys = null, Type = "global", Key = "global", Expiry = 1, }; actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); Assert.Equal("route", actual.Type); Assert.Equal("global", actual.Key); Assert.Equal(3, actual.ExpiryInMs); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void CreateProtected_NoOptions() { // Arrange var method = _creator.GetType().GetMethod("Create", BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new(); FileLoadBalancerOptions options = null; FileGlobalLoadBalancerOptions globalOptions = null; // Act var actual = (LoadBalancerOptions)method.Invoke(_creator, [route, options, globalOptions]); // Assert Assert.Equal(nameof(NoLoadBalancer), actual.Type); } [Fact] [Trait("PR", "2324")] [Trait("Feat", "2319")] public void Merge_NullCheck() { // Arrange var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); FileLoadBalancerOptions options = null; FileLoadBalancerOptions globalOptions = null; // Act, Assert 1 var wrapper = Assert.Throws( () => method.Invoke(_creator, [null, globalOptions])); Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(options), actual.ParamName); // Act, Assert 2 options = new(); wrapper = Assert.Throws( () => method.Invoke(_creator, [options, null])); Assert.IsType(wrapper.InnerException); actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(globalOptions), actual.ParamName); } [Theory] [Trait("PR", "2324")] [Trait("Feat", "2319")] [InlineData(false)] [InlineData(true)] public void Merge(bool isDef) { // Arrange var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); FileLoadBalancerOptions options = new() { Type = isDef ? string.Empty : "route", Key = isDef ? string.Empty : "route", Expiry = isDef ? null : 1, }; FileLoadBalancerOptions globalOptions = new("global") { Key = "global", Expiry = 3, }; // Act var actual = (LoadBalancerOptions)method.Invoke(_creator, [options, globalOptions]); // Assert Assert.Equal(isDef ? "global" : "route", actual.Type); Assert.Equal(isDef ? "global" : "route", actual.Key); Assert.Equal(isDef ? 3 : 1, actual.ExpiryInMs); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/LoadBalancerOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.LoadBalancer.Balancers; namespace Ocelot.UnitTests.LoadBalancer; public class LoadBalancerOptionsTests { [Fact] public void Ctor_ShouldDefaultToNoLoadBalancer() { // Arrange, Act LoadBalancerOptions options = new(); LoadBalancerOptions options2 = new(default, default, default); LoadBalancerOptions options3 = new(new FileLoadBalancerOptions()); // Assert Assert.Equal(nameof(NoLoadBalancer), options.Type); Assert.Equal(nameof(NoLoadBalancer), options2.Type); Assert.Equal(nameof(NoLoadBalancer), options3.Type); } [Fact] public void Ctor_Parameterless() { // Arrange, Act LoadBalancerOptions actual = new(); // Assert Assert.Equal(nameof(NoLoadBalancer), actual.Type); Assert.Null(actual.Key); Assert.Equal(0, actual.ExpiryInMs); } [Fact] [Trait("PR", "2324")] public void Ctor_CopyingFrom_FileLoadBalancerOptions() { // Arrange FileLoadBalancerOptions from = new() { Type = "Balancer", Key = "BalancerKey", Expiry = 3, }; // Act LoadBalancerOptions actual = new(from); // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equal("Balancer", actual.Type); Assert.Equal("BalancerKey", actual.Key); Assert.Equal(3, actual.ExpiryInMs); } [Theory] [Trait("PR", "2324")] [InlineData(false)] [InlineData(true)] public void Ctor_Initialization3Params(bool isDef) { // Arrange FileLoadBalancerOptions from = new() { Type = isDef ? string.Empty : "TestBalancer", Key = isDef ? string.Empty : "Bla-Bla", Expiry = isDef ? null : 3, }; // Act LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry); // Assert Assert.Equal(isDef ? nameof(NoLoadBalancer) : "TestBalancer", actual.Type); Assert.Equal(isDef ? string.Empty : "Bla-Bla", actual.Key); Assert.Equal(isDef ? 0 : 3, actual.ExpiryInMs); } [Theory] [Trait("PR", "2324")] [InlineData(false)] [InlineData(true)] public void Ctor_Initialization_CookieStickySessions(bool isDef) { // Arrange FileLoadBalancerOptions from = new() { Type = nameof(CookieStickySessions), Key = isDef ? string.Empty : "Key", Expiry = isDef ? null : 3, }; // Act LoadBalancerOptions actual = new(from.Type, from.Key, from.Expiry); // Assert Assert.Equal("CookieStickySessions", actual.Type); Assert.Equal(isDef ? ".AspNetCore.Session" : "Key", actual.Key); Assert.Equal(isDef ? 1200000 : 3, actual.ExpiryInMs); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerCreatorTests.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.LoadBalancer; public class NoLoadBalancerCreatorTests : UnitTest { private readonly NoLoadBalancerCreator _creator; private readonly Mock _serviceProvider; public NoLoadBalancerCreatorTests() { _creator = new NoLoadBalancerCreator(); _serviceProvider = new Mock(); } [Fact] public void Should_return_instance_of_expected_load_balancer_type() { // Arrange var route = new DownstreamRouteBuilder().Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.ShouldBeOfType(); } [Fact] public void Should_return_expected_name() { // Arrange, Act, Assert _creator.Type.ShouldBe(nameof(NoLoadBalancer)); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/NoLoadBalancerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer.Balancers; using Ocelot.Responses; using Ocelot.Values; namespace Ocelot.UnitTests.LoadBalancer; public class NoLoadBalancerTests : UnitTest { private readonly List _services; private NoLoadBalancer _loadBalancer; private Response _result; public NoLoadBalancerTests() { _services = new List(); _loadBalancer = new NoLoadBalancer(() => Task.FromResult(_services)); } [Fact] public async Task Should_return_host_and_port() { // Arrange var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); var services = new List { new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; _services.AddRange(services); // Act _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); // Assert _result.Data.ShouldBe(hostAndPort); } [Fact] public async Task Should_return_error_if_no_services() { // Arrange, Act _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); // Assert _result.IsError.ShouldBeTrue(); } [Fact] public async Task Should_return_error_if_no_services_then_when_services_available_return_host_and_port() { // Arrange var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); var services = new List { new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; // Act, Assert _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); _result.IsError.ShouldBeTrue(); _services.AddRange(services); // Act, Assert _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); _result.Data.ShouldBe(hostAndPort); } [Fact] public async Task Should_return_error_if_null_services() { // Arrange _loadBalancer = new NoLoadBalancer(() => Task.FromResult((List)null)); // Act _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); // Assert _result.IsError.ShouldBeTrue(); } [Fact] public async Task Release() { // Arrange var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); var services = new List { new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; _services.AddRange(services); _result = await _loadBalancer.LeaseAsync(new DefaultHttpContext()); // Act _loadBalancer.Release(hostAndPort); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/RoundRobinCreatorTests.cs ================================================ using Ocelot.Configuration.Builder; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Creators; using Ocelot.ServiceDiscovery.Providers; using System.Reflection; namespace Ocelot.UnitTests.LoadBalancer; public class RoundRobinCreatorTests : UnitTest { private readonly RoundRobinCreator _creator; private readonly Mock _serviceProvider; public RoundRobinCreatorTests() { _creator = new RoundRobinCreator(); _serviceProvider = new Mock(); } [Theory] [InlineData(false)] [InlineData(true)] public void Should_return_instance_of_expected_load_balancer_type(bool isNullServiceName) { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName(isNullServiceName ? null : "myService") .WithLoadBalancerKey("key") .Build(); // Act var loadBalancer = _creator.Create(route, _serviceProvider.Object); // Assert loadBalancer.Data.ShouldBeOfType(); var balancer = loadBalancer.Data as RoundRobin; var field = balancer.GetType().GetField("_serviceName", BindingFlags.Instance | BindingFlags.NonPublic); var serviceName = field.GetValue(balancer) as string; serviceName.ShouldBe(isNullServiceName ? "key" : "myService"); } [Fact] public void Should_return_expected_name() { // Arrange, Act, Assert _creator.Type.ShouldBe(nameof(RoundRobin)); } } ================================================ FILE: test/Ocelot.UnitTests/LoadBalancer/RoundRobinTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.LoadBalancer; using Ocelot.LoadBalancer.Balancers; using Ocelot.LoadBalancer.Errors; using Ocelot.LoadBalancer.Interfaces; using Ocelot.Responses; using Ocelot.Values; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.LoadBalancer; public class RoundRobinTests : UnitTest { private readonly DefaultHttpContext _httpContext = new(); [Fact] public async Task Lease_LoopThroughIndexRangeOnce_ShouldGetNextAddress() { // Arrange var services = GivenServices(); var roundRobin = GivenLoadBalancer(services); // Act var response0 = await roundRobin.LeaseAsync(_httpContext); var response1 = await roundRobin.LeaseAsync(_httpContext); var response2 = await roundRobin.LeaseAsync(_httpContext); // Assert response0.Data.ShouldNotBeNull().ShouldBe(services[0].HostAndPort); response1.Data.ShouldNotBeNull().ShouldBe(services[1].HostAndPort); response2.Data.ShouldNotBeNull().ShouldBe(services[2].HostAndPort); } [Fact] [Trait("Feat", "336")] public async Task Lease_LoopThroughIndexRangeIndefinitelyButOneSecond_ShouldGoBackToFirstAddressAfterFinishedLast() { // Arrange var services = GivenServices(); var roundRobin = GivenLoadBalancer(services); var stopWatch = Stopwatch.StartNew(); while (stopWatch.ElapsedMilliseconds < 1000) { // Act var response0 = await roundRobin.LeaseAsync(_httpContext); var response1 = await roundRobin.LeaseAsync(_httpContext); var response2 = await roundRobin.LeaseAsync(_httpContext); // Assert response0.Data.ShouldNotBeNull().ShouldBe(services[0].HostAndPort); response1.Data.ShouldNotBeNull().ShouldBe(services[1].HostAndPort); response2.Data.ShouldNotBeNull().ShouldBe(services[2].HostAndPort); } } [Fact] [Trait("Bug", "2110")] public async Task Lease_SelectedServiceIsNull_ShouldReturnError() { // Arrange var invalidServices = new List { null }; var roundRobin = GivenLoadBalancer(invalidServices); // Act var response = await roundRobin.LeaseAsync(_httpContext); // Assert: Then ServicesAreNullError Is Returned response.ShouldNotBeNull().Data.ShouldBeNull(); response.IsError.ShouldBeTrue(); response.Errors[0].ShouldBeOfType(); } //[InlineData(1, 10)] //[InlineData(2, 50)] //[InlineData(3, 50)] //[InlineData(4, 50)] //[InlineData(5, 50)] //[InlineData(3, 100)] //[InlineData(4, 100)] //[InlineData(7, 100)] [InlineData(3, 100)] [Theory] [Trait("Feat", "2110")] public void Lease_LoopThroughIndexRangeIndefinitelyUnderHighLoad_ShouldDistributeIndexValuesUniformly(int totalServices, int totalThreads) { // Arrange const bool ReturnServicesNotImmediately = false; var services = GivenServices(totalServices); var roundRobin = GivenLoadBalancer(services, ReturnServicesNotImmediately); int bottom = totalThreads / totalServices, top = totalThreads - (bottom * totalServices) + bottom; // Act var responses = WhenICallLeaseFromMultipleThreads(roundRobin, totalThreads); var counters = CountServices(services, responses); // Assert responses.ShouldNotBeNull(); responses.Length.ShouldBe(totalThreads); var message = $"All values are [{string.Join(',', counters)}]"; counters.Sum().ShouldBe(totalThreads, message); message = $"{nameof(bottom)}: {bottom}\n\t{nameof(top)}: {top}\n\tAll values are [{string.Join(',', counters)}]"; counters.ShouldAllBe(counter => bottom <= counter && counter <= top, message); } [Fact] public async Task OnLeased() { // Arrange const string ServiceName = "products"; var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), }; var roundRobin = new TestRoundRobin(() => Task.FromResult(availableServices), ServiceName); // Act var result = await roundRobin.LeaseAsync(_httpContext); // Assert Assert.NotEmpty(roundRobin.Events); var args = roundRobin.Events[0]; Assert.NotNull(args); Assert.Equal(ServiceName, args.Service.Name); } [Theory] [InlineData(false)] [InlineData(true)] public async Task LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError(bool isNull) { // Arrange List services = isNull ? null : GivenServices(0); var roundRobin = GivenLoadBalancer(services); // Act var actual = await roundRobin.LeaseAsync(_httpContext); // Assert Assert.True(actual.IsError); var error = actual.Errors[0]; Assert.IsType(error); Assert.Equal("There were no services in RoundRobin for 'LeaseAsync_ServicesAreEmpty_ServicesAreEmptyError' during LeaseAsync operation!", error.Message); } [Fact] public async Task Release() { // Arrange const string ServiceName = nameof(Release); var availableServices = new List { new(ServiceName, new ServiceHostAndPort("127.0.0.1", 80), string.Empty, string.Empty, Array.Empty()), }; var roundRobin = new RoundRobin(() => Task.FromResult(availableServices), ServiceName); var response = await roundRobin.LeaseAsync(_httpContext); // Act, Assert roundRobin.Release(response.Data); } [Fact] public void TryScanNext() { // Arrange const int lastIndex = 3; var method = typeof(RoundRobin).GetMethod(nameof(TryScanNext), BindingFlags.Instance | BindingFlags.NonPublic); var field = typeof(RoundRobin).GetField("LastIndices", BindingFlags.Static | BindingFlags.NonPublic); List services = GivenServices(lastIndex); var roundRobin = GivenLoadBalancer(services); var lastIndices = field.GetValue(roundRobin) as Dictionary; lastIndices[nameof(TryScanNext)] = lastIndex; // Act // TryScanNext(Service[] readme, out Service next, out int index) var readme = services.ToArray(); Service next = null; int index = -1; object[] parameters = [readme, next, index]; bool success = (bool)method.Invoke(roundRobin, parameters); // Assert Assert.True(success); Assert.Equal(0, parameters[2]); Assert.Equal(readme[0], parameters[1]); Assert.Equal(1, lastIndices[nameof(TryScanNext)]); } [Theory] [InlineData(false)] [InlineData(true)] public void Update_CanIncreaseConnections(bool increase) { var method = typeof(RoundRobin).GetMethod("Update", BindingFlags.Instance | BindingFlags.NonPublic); var field = typeof(RoundRobin).GetField("_leasing", BindingFlags.Instance | BindingFlags.NonPublic); List services = GivenServices(1); var roundRobin = GivenLoadBalancer(services); Lease item = new( services[0].HostAndPort, increase ? 0 : 1); var leasing = field.GetValue(roundRobin) as List; leasing.Add(item); // Act // int Update(ref Lease item, bool increase) object[] parameters = [item, increase]; int index = (int)method.Invoke(roundRobin, parameters); Lease actual = (Lease)parameters[0]; Assert.Equal(0, index); Assert.Equal(increase ? 1 : 0, actual.Connections); } private static int[] CountServices(List services, Response[] responses) { var counters = new int[services.Count]; var firstPort = services[0].HostAndPort.DownstreamPort; foreach (var response in responses) { var idx = response.Data.DownstreamPort - firstPort; counters[idx]++; } return counters; } private Response[] WhenICallLeaseFromMultipleThreads(RoundRobin roundRobin, int times) { var tasks = new Task[times]; // allocate N-times threads as Task var parallelResponses = new Response[times]; for (var i = 0; i < times; i++) { tasks[i] = GetParallelResponse(parallelResponses, roundRobin, i); } Task.WaitAll(tasks); // load by N-times threads return parallelResponses; } private async Task GetParallelResponse(Response[] responses, RoundRobin roundRobin, int threadIndex) { responses[threadIndex] = await roundRobin.LeaseAsync(_httpContext); } private static List GivenServices(int total = 3, [CallerMemberName] string serviceName = null) { var list = new List(total); for (int i = 1; i <= total; i++) { list.Add(new(serviceName, new ServiceHostAndPort("127.0.0." + i, 5000 + i), string.Empty, string.Empty, Array.Empty())); } return list; } private static RoundRobin GivenLoadBalancer(List services, bool immediately = true, [CallerMemberName] string serviceName = null) { return new( () => { int leasingDelay = immediately ? 0 : Random.Shared.Next(5, 15); Thread.Sleep(leasingDelay); return Task.FromResult(services); }, serviceName); } } internal sealed class TestRoundRobin : RoundRobin, ILoadBalancer { public readonly List Events = new(); public TestRoundRobin(Func>> services, string serviceName) : base(services, serviceName) => Leased += Me_Leased; private void Me_Leased(object sender, LeaseEventArgs args) => Events.Add(args); } ================================================ FILE: test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Logging; using System.Reflection; namespace Ocelot.UnitTests.Logging; public class OcelotDiagnosticListenerTests : UnitTest { private OcelotDiagnosticListener _listener; private readonly Mock _factory; private readonly Mock _logger; private readonly IServiceCollection _serviceCollection; private IServiceProvider _serviceProvider; private readonly DefaultHttpContext _httpContext; public OcelotDiagnosticListenerTests() { _httpContext = new DefaultHttpContext(); _factory = new Mock(); _logger = new Mock(); _serviceCollection = new ServiceCollection(); _serviceProvider = _serviceCollection.BuildServiceProvider(true); _factory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _listener = new OcelotDiagnosticListener(_factory.Object, _serviceProvider); } [Fact] public void Should_trace_middleware_started() { // Arrange const string name = "name"; // Act _listener.OnMiddlewareStarting(_httpContext, name); // Assert ThenTheLogIs($"MiddlewareStarting: {name}; {_httpContext.Request.Path}"); } [Fact] public void Should_trace_middleware_finished() { // Arrange const string name = "name"; // Act _listener.OnMiddlewareFinished(_httpContext, name); // Assert ThenTheLogIs($"MiddlewareFinished: {name}; {_httpContext.Response.StatusCode}"); } [Fact] public void Should_trace_middleware_exception() { // Arrange const string name = "name"; var exception = new Exception("oh no"); // Act _listener.OnMiddlewareException(exception, name); // Assert ThenTheLogIs($"MiddlewareException: {name}; {exception.Message};"); } [Fact] public void Event() { // Arrange var tracer = new Mock(); tracer.Setup(x => x.Event(It.IsAny(), It.IsAny())); var method = _listener.GetType().GetMethod(nameof(Event), BindingFlags.Instance | BindingFlags.NonPublic); // Act method.Invoke(_listener, [_httpContext, TestID]); // Assert 1 : _tracer is null tracer.Verify(x => x.Event(It.IsAny(), It.IsAny()), Times.Never); // Scenario 2: _tracer is NOT null _serviceCollection.AddSingleton(tracer.Object); _serviceProvider = _serviceCollection.BuildServiceProvider(true); _listener = new OcelotDiagnosticListener(_factory.Object, _serviceProvider); // Act method.Invoke(_listener, [_httpContext, TestID]); tracer.Verify(x => x.Event(It.IsAny(), It.IsAny()), Times.Once); } private void ThenTheLogIs(string expected) { _logger.Verify(x => x.LogTrace(It.Is>(c => c.Invoke() == expected))); } } ================================================ FILE: test/Ocelot.UnitTests/Logging/OcelotHttpTracingHandlerTests.cs ================================================ using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Responses; using System.Reflection; namespace Ocelot.UnitTests.Logging; public class OcelotHttpTracingHandlerTests : UnitTest { private OcelotHttpTracingHandler handler; private readonly Mock tracer = new(); private readonly Mock repo = new(); private readonly Mock httpMessageHandler = new(); public OcelotHttpTracingHandlerTests() { handler = new(tracer.Object, repo.Object, httpMessageHandler.Object); } [Fact] public void Ctor() { // Arrange, Act handler = new(tracer.Object, repo.Object, httpMessageHandler.Object); // Assert Assert.NotNull(handler.InnerHandler); Assert.True(ReferenceEquals(httpMessageHandler.Object, handler.InnerHandler)); } [Fact] public void Ctor_NullChecks() { // Arrange, Act, Assert: argument 1 var ex = Assert.Throws( () => handler = new(null, repo.Object, httpMessageHandler.Object)); Assert.Equal(nameof(tracer), ex.ParamName); // Arrange, Act, Assert: argument 2 ex = Assert.Throws( () => handler = new(tracer.Object, null, httpMessageHandler.Object)); Assert.Equal(nameof(repo), ex.ParamName); // Arrange, Act, Assert: argument 3 handler = new(tracer.Object, repo.Object, null); Assert.NotNull(handler.InnerHandler); Assert.IsType(handler.InnerHandler); } [Fact] public async Task SendAsync() { // Arrange var sendAsync = handler.GetType().GetMethod(nameof(SendAsync), BindingFlags.Instance | BindingFlags.NonPublic); HttpRequestMessage request = new(); CancellationToken token = CancellationToken.None; HttpResponseMessage responseMessage = new(); tracer.Setup(x => x.SendAsync(request, It.IsAny>(), It.IsAny>>(), It.IsAny())) .ReturnsAsync(responseMessage); repo.Setup(x => x.Add(It.IsAny(), It.IsAny())) .Returns(new OkResponse()); // Act var task = sendAsync.Invoke(handler, [request, token]) as Task; var actual = await task; // Assert Assert.NotNull(actual); Assert.Same(responseMessage, actual); tracer.Verify(x => x.SendAsync(request, It.IsAny>(), It.IsAny>>(), It.IsAny()), Times.Once); repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Never); } [Fact] public void AddTraceId() { // Arrange var addTraceId = handler.GetType().GetMethod(nameof(AddTraceId), BindingFlags.Instance | BindingFlags.NonPublic); Tuple added = null; repo.Setup(x => x.Add(It.IsAny(), It.IsAny())) .Callback((k, v) => added = new(k, v)) .Returns(new OkResponse()); // Act addTraceId.Invoke(handler, [TestID]); // Assert repo.Verify(x => x.Add(It.IsAny(), It.IsAny()), Times.Once); Assert.NotNull(added); Assert.Equal("TraceId", added.Item1); Assert.Equal(TestID, added.Item2); } } ================================================ FILE: test/Ocelot.UnitTests/Logging/OcelotLoggerFactoryTests.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; namespace Ocelot.UnitTests.Logging; public class OcelotLoggerFactoryTests { private readonly Mock _loggerFactoryMock; private readonly Mock _scopedDataRepositoryMock; private readonly OcelotLoggerFactory _factory; public OcelotLoggerFactoryTests() { _loggerFactoryMock = new Mock(); _scopedDataRepositoryMock = new Mock(); _factory = new OcelotLoggerFactory( _loggerFactoryMock.Object, _scopedDataRepositoryMock.Object); } [Fact] public void Constructor_GivenNullLoggerFactory_ArgumentNullExceptionThrown() { // Arrange ILoggerFactory loggerFactory = null; // Act + Assert var exception = Assert.Throws(() => new OcelotLoggerFactory(loggerFactory, _scopedDataRepositoryMock.Object)); // Optional: check the parameter name Assert.Equal(nameof(loggerFactory), exception.ParamName); } [Fact] public void Constructor_GivenNullScopedDataRepository_ArgumentNullExceptionThrown() { // Arrange IRequestScopedDataRepository scopedDataRepository = null; // Act + Assert var exception = Assert.Throws(() => new OcelotLoggerFactory(_loggerFactoryMock.Object, scopedDataRepository!)); // Optional: check the parameter name Assert.Equal(nameof(scopedDataRepository), exception.ParamName); } [Fact] public void CreateLogger_GivenGenericType_LoggerCreated() { // Arrange var logger = new Mock(); _loggerFactoryMock .Setup(x => x.CreateLogger(It.IsAny())) .Returns(logger.Object); // Act var result = _factory.CreateLogger(); // Assert Assert.NotNull(result); _loggerFactoryMock.Verify( x => x.CreateLogger(typeof(OcelotLoggerFactoryTests).FullName), Times.Once); } [Fact] public void CreateLogger_GivenLoggerFactoryThrows_ExceptionThrown() { // Arrange _loggerFactoryMock .Setup(x => x.CreateLogger(It.IsAny())) .Throws(new InvalidOperationException("logger creation failed")); // Act var exception = Record.Exception(() => _factory.CreateLogger()); // Assert Assert.NotNull(exception); Assert.IsType(exception); } [Fact] public void CreateLogger_GivenFactoryDisposed_ObjectDisposedExceptionThrown() { // Arrange _factory.Dispose(); // Act + Assert Assert.Throws(() => _factory.CreateLogger()); } [Fact] public void CreateLogger_GivenGenericType_OcelotLoggerReturned() { // Arrange var logger = new Mock(); _loggerFactoryMock .Setup(x => x.CreateLogger(It.IsAny())) .Returns(logger.Object); // Act var result = _factory.CreateLogger(); // Assert Assert.NotNull(result); Assert.IsType(result); } [Fact] public void Dispose_GivenFactoryNotDisposed_InnerFactoryDisposed() { // Arrange // Act _factory.Dispose(); // Assert _loggerFactoryMock.Verify(x => x.Dispose(), Times.Once); } [Fact] public void Dispose_GivenFactoryAlreadyDisposed_NoExceptionThrown() { // Arrange // Act var exception = Record.Exception(() => { _factory.Dispose(); _factory.Dispose(); }); // Assert Assert.Null(exception); } } ================================================ FILE: test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Responses; namespace Ocelot.UnitTests.Logging; public class OcelotLoggerTests { private static readonly string _a = "Tom"; private static readonly string _b = "Laura"; private static readonly Exception _ex = new("oh no"); private static readonly string NL = Environment.NewLine; private OcelotLogger _logger; private readonly Mock> logger = new(); private readonly Mock scopedDataRepository = new(); public OcelotLoggerTests() { logger.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); scopedDataRepository.Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse("1")); _logger = new OcelotLogger(logger.Object, scopedDataRepository.Object); } [Fact] public void Ctor_NullChecks() { // Arrange, Act, Assert: argument 1 var ex = Assert.Throws( () => _logger = new(null, scopedDataRepository.Object)); Assert.Equal(nameof(logger), ex.ParamName); // Arrange, Act, Assert: argument 2 ex = Assert.Throws( () => _logger = new(logger.Object, null)); Assert.Equal(nameof(scopedDataRepository), ex.ParamName); } [Fact] public void GetOcelotRequestId() { // Arrange, Act, Assert scopedDataRepository.Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse("X")); _logger.LogTrace($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: X, PreviousRequestId: X{NL}a message from Tom to Laura", LogLevel.Trace, Times.Once()); scopedDataRepository.Setup(x => x.Get(It.IsAny())) .Returns(new ErrorResponse(new CannotFindDataError("error"))); _logger.LogTrace($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: -, PreviousRequestId: -{NL}a message from Tom to Laura", LogLevel.Trace, Times.Once()); } [Fact] public void Should_log_trace() { // Arrange, Act, Assert _logger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Trace, Times.Once()); _logger.LogTrace($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Trace, Times.Exactly(2)); } [Fact] public void Should_log_debug() { // Arrange, Act, Assert _logger.LogDebug(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Debug, Times.Once()); _logger.LogDebug($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Debug, Times.Exactly(2)); } [Fact] public void Should_log_info() { // Arrange, Act, Assert _logger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Information, Times.Once()); _logger.LogInformation($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Information, Times.Exactly(2)); } [Fact] public void Should_log_warning() { // Arrange, Act, Assert _logger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Warning, Times.Once()); _logger.LogWarning($"a message from {_a} to {_b}"); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Warning, Times.Exactly(2)); } [Fact] public void Should_log_error() { // Arrange, Act, Assert _logger.LogError(() => $"a message from {_a} to {_b}", _ex); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Error, Times.Once(), _ex); _logger.LogError($"a message from {_a} to {_b}", _ex); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Error, Times.Exactly(2), _ex); } [Fact] public void Should_log_critical() { // Arrange, Act, Assert _logger.LogCritical(() => $"a message from {_a} to {_b}", _ex); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Critical, Times.Once(), _ex); _logger.LogCritical($"a message from {_a} to {_b}", _ex); ThenLevelIsLogged($"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura", LogLevel.Critical, Times.Exactly(2), _ex); } [Fact] public void StaticFormatters() { Exception ex = new("test"); var actual = OcelotLogger.NoFormatter("x", ex); Assert.Equal("x", actual); actual = OcelotLogger.ExceptionFormatter("y", null); Assert.Equal("y", actual); actual = OcelotLogger.ExceptionFormatter("z", ex); var expected = $"z, {Environment.NewLine}Exception: System.Exception: test"; Assert.Equal(expected, actual); } /// /// Here mocking the original logger implementation to verify calls. /// /// The chosen minimum log level. /// A mocked object. private static Mock> MockLogger(LogLevel? minimumLevel) { var logger = LoggerFactory.Create(builder => { if (minimumLevel.HasValue) { builder .AddSimpleConsole() .SetMinimumLevel(minimumLevel.Value); } else { builder.AddSimpleConsole(); } }) .CreateLogger>(); var mockedILogger = new Mock>(); mockedILogger.Setup(x => x.IsEnabled(It.IsAny())) .Returns(logger.IsEnabled) .Verifiable(); return mockedILogger; } [Fact] public void If_minimum_log_level_not_set_then_log_is_called_for_information_and_above() { var mockedILogger = MockLogger(null); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void If_minimum_log_level_set_to_none_then_log_method_is_never_called() { var mockedILogger = MockLogger(LogLevel.None); var repo = new Mock(); var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from Tom to Laura'"; ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void If_minimum_log_level_set_to_trace_then_log_is_called_for_trace_and_above() { var mockedILogger = MockLogger(LogLevel.Trace); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void String_func_is_never_called_when_log_level_is_disabled() { // Arrange var mockedFunc = new Mock>(); mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); var mockedILogger = MockLogger(LogLevel.None); var repo = new Mock(); var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); // Act currentLogger.LogTrace(mockedFunc.Object); // Assert mockedFunc.Verify(x => x.Invoke(), Times.Never); } [Fact] public void String_func_is_called_once_when_log_level_is_enabled() { // Arrange var mockedFunc = new Mock>(); mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); var mockedILogger = MockLogger(LogLevel.Information); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); // Act currentLogger.LogInformation(mockedFunc.Object); // Assert mockedFunc.Verify(x => x.Invoke(), Times.Once); } [Fact] public void If_minimum_log_level_set_to_debug_then_log_is_called_for_debug_and_above() { var mockedILogger = MockLogger(LogLevel.Debug); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void If_minimum_log_level_set_to_warning_then_log_is_called_for_warning_and_above() { var mockedILogger = MockLogger(LogLevel.Warning); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void If_minimum_log_level_set_to_error_then_log_is_called_for_error_and_above() { var mockedILogger = MockLogger(LogLevel.Error); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } [Fact] public void If_minimum_log_level_set_to_critical_then_log_is_called_for_critical_and_above() { var mockedILogger = MockLogger(LogLevel.Critical); var currentLogger = new OcelotLogger(mockedILogger.Object, scopedDataRepository.Object); currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); var expected = $"RequestId: 1, PreviousRequestId: 1{NL}a message from Tom to Laura"; ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); var testException = new Exception("test"); currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); } private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Times times, Exception ex = null) { logger.Verify( x => x.Log(expectedLogLevel, default, expected, ex, It.IsAny>()), times); } private static void ThenLevelIsLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) { logger.Verify( x => x.Log( expectedLogLevel, default, expected, ex, It.IsAny>()), Times.Once); } private static void ThenLevelIsNotLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) { var result = logger.Object.IsEnabled(expectedLogLevel); logger.Verify( x => x.Log( expectedLogLevel, default, expected, ex, It.IsAny>()), Times.Never); } } ================================================ FILE: test/Ocelot.UnitTests/Logging/OcelotLoggerTestsForDisposal.cs ================================================ using Microsoft.Extensions.Logging; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Responses; namespace Ocelot.UnitTests.Logging; public class OcelotLoggerTestsForDisposal { private readonly Mock _innerLoggerMock; private readonly Mock _scopedDataRepositoryMock; private readonly OcelotLogger _logger; public OcelotLoggerTestsForDisposal() { _innerLoggerMock = new Mock(); _innerLoggerMock .Setup(x => x.IsEnabled(It.IsAny())) .Returns(true); _scopedDataRepositoryMock = new Mock(); _scopedDataRepositoryMock .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse("ID")); _logger = new OcelotLogger(_innerLoggerMock.Object, _scopedDataRepositoryMock.Object); } [Fact] public void Dispose_GivenLoggerDisposedThenLoggingAttempt_NoUnderlyingLoggerCalled() { // Arrange _logger.Dispose(); // Act _logger.LogInformation("should not log"); // Assert _innerLoggerMock.Verify(x => x.Log( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Never); } [Fact] public void Dispose_CalledMultipleTimes_NoExceptionThrown() { // Arrange _logger.Dispose(); // Act & Assert Assert.Null(Record.Exception(() => { _logger.Dispose(); _logger.Dispose(); })); } [Fact] public void LogAfterDisposeWithFunc_GivenDisposed_NoFuncInvocation() { // Arrange var funcMock = new Mock>(); funcMock.Setup(x => x.Invoke()).Returns("invoked"); _logger.Dispose(); // Act _logger.LogTrace(funcMock.Object); // Assert funcMock.Verify(x => x.Invoke(), Times.Never); _innerLoggerMock.Verify(x => x.Log( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Never); } [Fact] public void Log_GivenLoggerThrowsObjectDisposedException_NoExceptionEscapes() { // Arrange // Configure the logger to throw ObjectDisposedException when logging _innerLoggerMock .Setup(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>())) .Throws(new ObjectDisposedException($"ILogger {nameof(_innerLoggerMock)}")); // Act & Assert: No exception escapes var exception = Record.Exception(() => _logger.LogInformation("Test message")); Assert.Null(exception); // Optional: also verify no call to underlying logger occurred (since Log threw, nothing should be invoked) _innerLoggerMock.Verify(x => x.Log(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Logging/TracingHandlerFactoryTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; namespace Ocelot.UnitTests.Logging; public class TracingHandlerFactoryTests { private readonly TracingHandlerFactory _factory; private readonly Mock _tracer; private readonly IServiceCollection _serviceCollection; private readonly IServiceProvider _serviceProvider; private readonly Mock _repo; public TracingHandlerFactoryTests() { _tracer = new Mock(); _serviceCollection = new ServiceCollection(); _serviceCollection.AddSingleton(_tracer.Object); _serviceProvider = _serviceCollection.BuildServiceProvider(true); _repo = new Mock(); _factory = new TracingHandlerFactory(_serviceProvider, _repo.Object); } [Fact] public void Should_return() { // Arrange, Act var handler = _factory.Get(); // Assert Assert.IsType(handler); } } ================================================ FILE: test/Ocelot.UnitTests/Middleware/BaseUrlFinderTests.cs ================================================ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Ocelot.Middleware; namespace Ocelot.UnitTests.Middleware; public class BaseUrlFinderTests : UnitTest { private BaseUrlFinder _baseUrlFinder; private IConfiguration _config; private readonly List> _data; public BaseUrlFinderTests() { _data = new List>(); } [Fact] public void Should_use_default_base_url() { var result = WhenIFindTheUrl(); result.ShouldBe("http://localhost:5000"); } [Fact] public void Should_use_memory_config_base_url() { GivenTheMemoryBaseUrlIs("http://baseurlfromconfig.com:5181"); var result = WhenIFindTheUrl(); result.ShouldBe("http://baseurlfromconfig.com:5181"); } [Fact] public void Should_use_file_config_base_url() { GivenTheMemoryBaseUrlIs("http://localhost:7000"); GivenTheFileBaseUrlIs("http://baseurlfromconfig.com:5181"); var result = WhenIFindTheUrl(); result.ShouldBe("http://baseurlfromconfig.com:5181"); } private void GivenTheMemoryBaseUrlIs(string configValue) { _data.Add(new KeyValuePair("BaseUrl", configValue)); } private void GivenTheFileBaseUrlIs(string configValue) { _data.Add(new KeyValuePair("GlobalConfiguration:BaseUrl", configValue)); } private string WhenIFindTheUrl() { var source = new MemoryConfigurationSource { InitialData = _data, }; var provider = new MemoryConfigurationProvider(source); _config = new ConfigurationRoot(new List { provider, }); _baseUrlFinder = new BaseUrlFinder(_config); return _baseUrlFinder.Find(); } } ================================================ FILE: test/Ocelot.UnitTests/Middleware/OcelotPipelineExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.DownstreamRouteFinder.Middleware; using Ocelot.DownstreamUrlCreator; using Ocelot.LoadBalancer; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.WebSockets; namespace Ocelot.UnitTests.Middleware; public class OcelotPipelineExtensionsTests : UnitTest { private ApplicationBuilder _builder; private RequestDelegate _handlers; [Fact] public void Should_set_up_pipeline() { // Arrange GivenTheDepedenciesAreSetUp(); // Act _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); // Assert _handlers.ShouldNotBeNull(); } [Fact] public void Should_expand_pipeline() { // Arrange GivenTheDepedenciesAreSetUp(); var configuration = new OcelotPipelineConfiguration(); configuration.MapWhenOcelotPipeline.Add((httpContext) => httpContext.WebSockets.IsWebSocketRequest, app => { app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); }); // Act _handlers = _builder.BuildOcelotPipeline(new OcelotPipelineConfiguration()); // Assert _handlers.ShouldNotBeNull(); } private void GivenTheDepedenciesAreSetUp() { var root = new ConfigurationBuilder().Build(); var services = new ServiceCollection(); services.AddSingleton(root); services.AddOcelot(); var provider = services.BuildServiceProvider(true); _builder = new ApplicationBuilder(provider); } } ================================================ FILE: test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.DependencyInjection; using Ocelot.Errors.Middleware; using Ocelot.Logging; using Ocelot.Middleware; using System.Reflection; namespace Ocelot.UnitTests.Middleware; public class OcelotPiplineBuilderTests : UnitTest { private readonly IServiceCollection _services; private readonly IConfiguration _configRoot; private int _counter; private readonly DefaultHttpContext _httpContext; public OcelotPiplineBuilderTests() { _configRoot = new ConfigurationRoot(new List()); _services = new ServiceCollection(); _services.AddSingleton(GetHostingEnvironment()); _services.AddSingleton(_configRoot); _services.AddOcelot(); _httpContext = new DefaultHttpContext(); } private static IWebHostEnvironment GetHostingEnvironment() { var environment = new Mock(); environment .Setup(e => e.ApplicationName) .Returns(typeof(OcelotPiplineBuilderTests).GetTypeInfo().Assembly.GetName().Name); return environment.Object; } [Fact] public void Should_build_generic() { // Arrange var provider = _services.BuildServiceProvider(true); IApplicationBuilder builder = new ApplicationBuilder(provider); builder = builder.UseMiddleware(); // Act var del = builder.Build(); del.Invoke(_httpContext); // Assert _httpContext.Response.StatusCode.ShouldBe(500); } [Fact] public void Should_build_func() { // Arrange _counter = 0; var provider = _services.BuildServiceProvider(true); IApplicationBuilder builder = new ApplicationBuilder(provider); builder = builder.Use(async (ctx, next) => { _counter++; await next.Invoke(); }); // Act var del = builder.Build(); del.Invoke(_httpContext); // Assert _counter.ShouldBe(1); _httpContext.Response.StatusCode.ShouldBe(404); } [Fact] public void Middleware_Multi_Parameters_Invoke() { // Arrange var provider = _services.BuildServiceProvider(true); IApplicationBuilder builder = new ApplicationBuilder(provider); builder = builder.UseMiddleware(); // Act, Assert var del = builder.Build(); del.Invoke(_httpContext); } private class MultiParametersInvokeMiddleware : OcelotMiddleware { #pragma warning disable IDE0060 // Remove unused parameter public MultiParametersInvokeMiddleware(RequestDelegate next) : base(new FakeLogger()) { } #pragma warning disable CA1822 // Mark members as static public Task Invoke(HttpContext context, IServiceProvider serviceProvider) => Task.CompletedTask; #pragma warning restore CA1822 // Mark members as static #pragma warning restore IDE0060 // Remove unused parameter } } internal class FakeLogger : IOcelotLogger { public void LogCritical(string message, Exception exception) { } public void LogCritical(Func messageFactory, Exception exception) { } public void LogError(string message, Exception exception) { } public void LogError(Func messageFactory, Exception exception) { } public void LogDebug(string message) { } public void LogDebug(Func messageFactory) { } public void LogInformation(string message) { } public void LogInformation(Func messageFactory) { } public void LogWarning(string message) { } public void LogTrace(string message) { } public void LogTrace(Func messageFactory) { } public void LogWarning(Func messageFactory) { } public void Dispose() { } } ================================================ FILE: test/Ocelot.UnitTests/Multiplexing/DefinedAggregatorProviderTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Multiplexer; using static Ocelot.UnitTests.Multiplexing.UserDefinedResponseAggregatorTests; namespace Ocelot.UnitTests.Multiplexing; public class DefinedAggregatorProviderTests : UnitTest { private ServiceLocatorDefinedAggregatorProvider _provider; [Fact] public void Should_find_aggregator() { // Arrange var route = new Route() { Aggregator = "TestDefinedAggregator", }; var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); var services = serviceCollection.BuildServiceProvider(true); _provider = new ServiceLocatorDefinedAggregatorProvider(services); // Act var aggregator = _provider.Get(route); // Assert aggregator.Data.ShouldNotBeNull(); aggregator.Data.ShouldBeOfType(); aggregator.IsError.ShouldBeFalse(); } [Fact] public void Should_not_find_aggregator() { // Arrange var route = new Route() { Aggregator = "TestDefinedAggregator", }; // Arrange: Given No Defined Aggregator var serviceCollection = new ServiceCollection(); var services = serviceCollection.BuildServiceProvider(true); _provider = new ServiceLocatorDefinedAggregatorProvider(services); // Act var aggregator = _provider.Get(route); // Assert aggregator.IsError.ShouldBeTrue(); aggregator.Errors[0].Message.ShouldBe("Could not find Aggregator: TestDefinedAggregator"); aggregator.Errors[0].ShouldBeOfType(); } } ================================================ FILE: test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Moq.Protected; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Multiplexer; using System.Reflection; using System.Security.Claims; using System.Text; namespace Ocelot.UnitTests.Multiplexing; public class MultiplexingMiddlewareTests : UnitTest { private MultiplexingMiddleware _middleware; private Ocelot.DownstreamRouteFinder.DownstreamRouteHolder _downstreamRoute; private int _count; private readonly DefaultHttpContext _httpContext; private readonly Mock factory; private readonly Mock aggregator; private readonly Mock loggerFactory; private readonly Mock logger; public MultiplexingMiddlewareTests() { _httpContext = new DefaultHttpContext(); factory = new Mock(); aggregator = new Mock(); factory.Setup(x => x.Get(It.IsAny())).Returns(aggregator.Object); loggerFactory = new Mock(); logger = new Mock(); loggerFactory.Setup(x => x.CreateLogger()).Returns(logger.Object); _middleware = new MultiplexingMiddleware(Next, loggerFactory.Object, factory.Object); } private Task Next(HttpContext context) => Task.FromResult(_count++); [Fact] public async Task Should_multiplex() { var route = GivenDefaultRoute(2); GivenTheFollowing(route); // Act await _middleware.Invoke(_httpContext); _count.ShouldBe(2); } [Fact] public async Task Should_not_multiplex() { var route = new Route(new DownstreamRouteBuilder().Build()); GivenTheFollowing(route); // Act await _middleware.Invoke(_httpContext); _count.ShouldBe(1); } [Fact] [Trait("Bug", "1396")] public async Task CreateThreadContextAsync_CopyUser_ToTarget() { var route = new DownstreamRouteBuilder().Build(); // Arrange GivenUser("test", "Copy", nameof(CreateThreadContextAsync_CopyUser_ToTarget)); // Act var method = _middleware.GetType().GetMethod("CreateThreadContextAsync", BindingFlags.NonPublic | BindingFlags.Instance); var actual = await (Task)method.Invoke(_middleware, new object[] { _httpContext, route }); // Assert AssertUsers(actual); } [Fact] [Trait("Bug", "1396")] public async Task Invoke_ContextUser_ForwardedToDownstreamContext() { // Create HttpContext actualContext = null; _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object); Task NextMe(HttpContext context) { actualContext = context; return Next(context); } // Arrange GivenUser("test", "Invoke", nameof(Invoke_ContextUser_ForwardedToDownstreamContext)); GivenTheFollowing(GivenDefaultRoute(2)); // Act await _middleware.Invoke(_httpContext); // Assert _count.ShouldBe(2); AssertUsers(actualContext); } [Fact] [Trait("PR", "1826")] public async Task Should_Not_Copy_Context_If_One_Downstream_Route() { _middleware = new MultiplexingMiddleware(NextMe, loggerFactory.Object, factory.Object); Task NextMe(HttpContext context) { Assert.Equal(_httpContext, context); return Next(context); } // Arrange GivenUser("test", "Invoke", nameof(Should_Not_Copy_Context_If_One_Downstream_Route)); GivenTheFollowing(GivenDefaultRoute(1)); // Act await _middleware.Invoke(_httpContext); // Assert _count.ShouldBe(1); } [Fact] [Trait("PR", "1826")] public async Task Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route() { var mock = MockMiddlewareFactory(null, null); _middleware = mock.Object; // Arrange GivenUser("test", "Invoke", nameof(Should_Call_ProcessSingleRoute_Once_If_One_Downstream_Route)); GivenTheFollowing(GivenDefaultRoute(1)); // Act await _middleware.Invoke(_httpContext); // Assert mock.Protected().Verify("ProcessSingleRouteAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); } [Theory] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)] [Trait("PR", "1826")] public async Task Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route(int routesCount) { var mock = MockMiddlewareFactory(null, null); // Arrange GivenUser("test", "Invoke", nameof(Should_Not_Call_ProcessSingleRoute_If_More_Than_One_Downstream_Route)); GivenTheFollowing(GivenDefaultRoute(routesCount)); // Act await _middleware.Invoke(_httpContext); // Assert mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), ItExpr.IsAny(), ItExpr.IsAny()); } [Theory] [InlineData(2)] [InlineData(3)] [InlineData(4)] [InlineData(5)] [Trait("PR", "1826")] public async Task Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once(int routesCount) { var mock = MockMiddlewareFactory(routesCount, null); // Arrange GivenUser("test", "Invoke", nameof(Should_Create_As_Many_Contexts_As_Routes_And_Map_Is_Called_Once)); GivenTheFollowing(GivenDefaultRoute(routesCount)); // Act await _middleware.Invoke(_httpContext); // Assert mock.Protected().Verify("MapAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.Is>(list => list.Count == routesCount) ); } [Fact] [Trait("PR", "1826")] public async Task Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route() { var mock = MockMiddlewareFactory(null, null); // Arrange GivenUser("test", "Invoke", nameof(Should_Not_Call_ProcessSingleRoute_Or_Map_If_No_Route)); GivenTheFollowing(GivenDefaultRoute(0)); // Act await _middleware.Invoke(_httpContext); // Assert mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), ItExpr.IsAny(), ItExpr.IsAny()); mock.Protected().Verify("MapAsync", Times.Never(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny>()); } [Theory] [Trait("Bug", "2039")] [InlineData(1)] // Times.Never() [InlineData(2)] // Times.Exactly(2) [InlineData(3)] // Times.Exactly(3) [InlineData(4)] // Times.Exactly(4) public async Task Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests(int numberOfRoutes) { // Arrange var mock = MockMiddlewareFactory(null, null); GivenUser("test", "Invoke", nameof(Should_Call_CloneRequestBodyAsync_Each_Time_Per_Requests)); GivenTheFollowing(GivenDefaultRoute(numberOfRoutes)); // Act await _middleware.Invoke(_httpContext); // Assert mock.Protected().Verify>("CloneRequestBodyAsync", numberOfRoutes > 1 ? Times.Exactly(numberOfRoutes) : Times.Never(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny()); } [Fact] [Trait("PR", "1826")] public async Task If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times() { var mock = MockMiddlewareFactory(null, AggregateRequestDelegateFactory()); // Arrange GivenUser("test", "Invoke", nameof(If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_Called_Map_Once_And_Pipeline_3_Times)); GivenTheFollowing(GivenRoutesWithAggregator()); // Act await _middleware.Invoke(_httpContext); mock.Protected().Verify("ProcessSingleRouteAsync", Times.Never(), ItExpr.IsAny(), ItExpr.IsAny()); mock.Protected().Verify("MapAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny(), ItExpr.IsAny>()); _count.ShouldBe(3); } private RequestDelegate AggregateRequestDelegateFactory() { return context => { var responseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]"; context.Items.Add("DownstreamResponse", new DownstreamResponse(new StringContent(responseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List
(), "test")); if (!context.Items.ContainsKey("TemplatePlaceholderNameAndValues")) { context.Items.Add("TemplatePlaceholderNameAndValues", new List()); } _count++; return Task.CompletedTask; }; } private Mock MockMiddlewareFactory(int? downstreamRoutesCount, RequestDelegate requestDelegate) { requestDelegate ??= Next; var mock = new Mock(requestDelegate, loggerFactory.Object, factory.Object) { CallBase = true }; mock.Protected().Setup("MapAsync", ItExpr.IsAny(), ItExpr.IsAny(), downstreamRoutesCount == null ? ItExpr.IsAny>() : ItExpr.Is>(list => list.Count == downstreamRoutesCount) ).Returns(Task.CompletedTask).Verifiable(); mock.Protected().Setup("ProcessSingleRouteAsync", ItExpr.IsAny(), ItExpr.IsAny() ).Returns(Task.CompletedTask).Verifiable(); _middleware = mock.Object; return mock; } private void GivenUser(string authentication, string name, string role) { var user = new ClaimsPrincipal(); user.AddIdentity(new(authentication, name, role)); _httpContext.User = user; } private void AssertUsers(HttpContext actual) { Assert.NotNull(actual); Assert.Same(_httpContext.User, actual.User); Assert.NotNull(actual.User.Identity); var identity = _httpContext.User.Identity as ClaimsIdentity; var actualIdentity = actual.User.Identity as ClaimsIdentity; Assert.Equal(identity.AuthenticationType, actualIdentity.AuthenticationType); Assert.Equal(identity.NameClaimType, actualIdentity.NameClaimType); Assert.Equal(identity.RoleClaimType, actualIdentity.RoleClaimType); } private static Route GivenDefaultRoute(int count) { var r = new Route(); for (var i = 0; i < count; i++) { r.DownstreamRoute.Add(new DownstreamRouteBuilder().Build()); } return r; } private static Route GivenRoutesWithAggregator() { var route1 = new DownstreamRouteBuilder().WithKey("Comments").Build(); var route2 = new DownstreamRouteBuilder().WithKey("UserDetails").Build(); var route3 = new DownstreamRouteBuilder().WithKey("PostDetails").Build(); return new Route() { DownstreamRoute = [route1, route2, route3], DownstreamRouteConfig = [ new AggregateRouteConfig { RouteKey = "UserDetails", JsonPath = "$[*].writerId", Parameter = "userId" }, new AggregateRouteConfig { RouteKey = "PostDetails", JsonPath = "$[*].postId", Parameter = "postId" }, ], Aggregator = "TestAggregator", }; } private void GivenTheFollowing(Route route) { _downstreamRoute = new Ocelot.DownstreamRouteFinder.DownstreamRouteHolder(new List(), route); _httpContext.Items.UpsertDownstreamRoute(_downstreamRoute); } } ================================================ FILE: test/Ocelot.UnitTests/Multiplexing/ResponseAggregatorFactoryTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Multiplexer; namespace Ocelot.UnitTests.Multiplexing; public class ResponseAggregatorFactoryTests : UnitTest { private readonly InMemoryResponseAggregatorFactory _factory; private readonly Mock _provider; private IResponseAggregator _aggregator; public ResponseAggregatorFactoryTests() { _provider = new Mock(); _aggregator = new SimpleJsonResponseAggregator(); _factory = new InMemoryResponseAggregatorFactory(_provider.Object, _aggregator); } [Fact] public void Should_return_simple_json_aggregator() { // Arrange var route = new Route(); // Act _aggregator = _factory.Get(route); // Assert _aggregator.ShouldBeOfType(); } [Fact] public void Should_return_user_defined_aggregator() { // Arrange var route = new Route() { Aggregator = "doesntmatter", }; // Act _aggregator = _factory.Get(route); // Assert _aggregator.ShouldBeOfType(); } } ================================================ FILE: test/Ocelot.UnitTests/Multiplexing/SimpleJsonResponseAggregatorTests.cs ================================================ using Castle.Components.DictionaryAdapter; using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Middleware; using Ocelot.Multiplexer; using Ocelot.UnitTests.Responder; using Ocelot.Values; using System.Text; namespace Ocelot.UnitTests.Multiplexing; public class SimpleJsonResponseAggregatorTests : UnitTest { private readonly SimpleJsonResponseAggregator _aggregator; public SimpleJsonResponseAggregatorTests() { _aggregator = new SimpleJsonResponseAggregator(); } [Fact] public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context_withConfig() { var commentsDownstreamRoute = new DownstreamRouteBuilder().WithKey("Comments").Build(); var userDetailsDownstreamRoute = new DownstreamRouteBuilder().WithKey("UserDetails") .WithUpstreamPathTemplate(new UpstreamPathTemplate(string.Empty, 0, false, "/v1/users/{userId}")) .Build(); var downstreamRoutes = new List { commentsDownstreamRoute, userDetailsDownstreamRoute, }; var route = new Route() { DownstreamRoute = downstreamRoutes, DownstreamRouteConfig = [ new(){RouteKey = "UserDetails",JsonPath = "$[*].writerId",Parameter = "userId"}, ], }; var commentsResponseContent = @"[{string.Emptyidstring.Empty:1,string.EmptywriterIdstring.Empty:1,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext1string.Empty},{string.Emptyidstring.Empty:2,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:2,string.Emptytextstring.Empty:string.Emptytext2string.Empty},{string.Emptyidstring.Empty:3,string.EmptywriterIdstring.Empty:2,string.EmptypostIdstring.Empty:1,string.Emptytextstring.Empty:string.Emptytext21string.Empty}]"; var commentsDownstreamContext = new DefaultHttpContext(); commentsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(commentsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new EditableList>>(), "some reason")); commentsDownstreamContext.Items.UpsertDownstreamRoute(commentsDownstreamRoute); var userDetailsResponseContent = @"[{string.Emptyidstring.Empty:1,string.EmptyfirstNamestring.Empty:string.Emptyabolfazlstring.Empty,string.EmptylastNamestring.Empty:string.Emptyrajabpourstring.Empty},{string.Emptyidstring.Empty:2,string.EmptyfirstNamestring.Empty:string.Emptyrezastring.Empty,string.EmptylastNamestring.Empty:string.Emptyrezaeistring.Empty}]"; var userDetailsDownstreamContext = new DefaultHttpContext(); userDetailsDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent(userDetailsResponseContent, Encoding.UTF8, "application/json"), HttpStatusCode.OK, new List>>(), "some reason")); userDetailsDownstreamContext.Items.UpsertDownstreamRoute(userDetailsDownstreamRoute); var downstreamContexts = new List { commentsDownstreamContext, userDetailsDownstreamContext }; var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + "}"; var upstreamContext = new DefaultHttpContext(); // Act await _aggregator.Aggregate(route, upstreamContext, downstreamContexts); // Assert await ThenTheContentIs(upstreamContext, expected); ThenTheContentTypeIs(upstreamContext, "application/json"); ThenTheReasonPhraseIs(upstreamContext, "cannot return from aggregate..which reason phrase would you use?"); } [Fact] public async Task Should_aggregate_n_responses_and_set_response_content_on_upstream_context() { var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); var downstreamRoutes = new List { billDownstreamRoute, georgeDownstreamRoute, }; var route = new Route() { DownstreamRoute = downstreamRoutes, }; var billDownstreamContext = new DefaultHttpContext(); billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new EditableList>>(), "some reason")); billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute); var georgeDownstreamContext = new DefaultHttpContext(); georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("George says hi"), HttpStatusCode.OK, new List>>(), "some reason")); georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute); var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; var expected = "{\"Bill\":Bill says hi,\"George\":George says hi}"; var upstreamContext = new DefaultHttpContext(); // Act await _aggregator.Aggregate(route, upstreamContext, downstreamContexts); // Assert await ThenTheContentIs(upstreamContext, expected); ThenTheContentTypeIs(upstreamContext, "application/json"); ThenTheReasonPhraseIs(upstreamContext, "cannot return from aggregate..which reason phrase would you use?"); } [Fact] public async Task Should_return_error_if_any_downstreams_have_errored() { var billDownstreamRoute = new DownstreamRouteBuilder().WithKey("Bill").Build(); var georgeDownstreamRoute = new DownstreamRouteBuilder().WithKey("George").Build(); var downstreamRoutes = new List { billDownstreamRoute, georgeDownstreamRoute, }; var route = new Route() { DownstreamRoute = downstreamRoutes, }; var billDownstreamContext = new DefaultHttpContext(); billDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Bill says hi"), HttpStatusCode.OK, new List>>(), "some reason")); billDownstreamContext.Items.UpsertDownstreamRoute(billDownstreamRoute); var georgeDownstreamContext = new DefaultHttpContext(); georgeDownstreamContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Error"), HttpStatusCode.OK, new List>>(), "some reason")); georgeDownstreamContext.Items.UpsertDownstreamRoute(georgeDownstreamRoute); georgeDownstreamContext.Items.SetError(new AnyError()); var downstreamContexts = new List { billDownstreamContext, georgeDownstreamContext }; var expected = "Error"; var upstreamContext = new DefaultHttpContext(); // Act await _aggregator.Aggregate(route, upstreamContext, downstreamContexts); // Assert await ThenTheContentIs(upstreamContext, expected); ThenTheErrorIsMapped(upstreamContext, downstreamContexts); } private static void ThenTheReasonPhraseIs(DefaultHttpContext upstreamContext, string expected) { upstreamContext.Items.DownstreamResponse().ReasonPhrase.ShouldBe(expected); } private static void ThenTheErrorIsMapped(DefaultHttpContext upstreamContext, List downstreamContexts) { upstreamContext.Items.Errors().ShouldBe(downstreamContexts[1].Items.Errors()); upstreamContext.Items.DownstreamResponse().ShouldBe(downstreamContexts[1].Items.DownstreamResponse()); } private static async Task ThenTheContentIs(DefaultHttpContext upstreamContext, string expected) { var content = await upstreamContext.Items.DownstreamResponse().Content.ReadAsStringAsync(); content.ShouldBe(expected); } private static void ThenTheContentTypeIs(DefaultHttpContext upstreamContext, string expected) { upstreamContext.Items.DownstreamResponse().Content.Headers.ContentType.MediaType.ShouldBe(expected); } } ================================================ FILE: test/Ocelot.UnitTests/Multiplexing/UserDefinedResponseAggregatorTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Middleware; using Ocelot.Multiplexer; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Multiplexing; public class UserDefinedResponseAggregatorTests : UnitTest { private readonly UserDefinedResponseAggregator _aggregator; private readonly Mock _provider; public UserDefinedResponseAggregatorTests() { _provider = new Mock(); _aggregator = new UserDefinedResponseAggregator(_provider.Object); } [Fact] public async Task Should_call_aggregator() { // Arrange var route = new Route(); var context = new DefaultHttpContext(); var contextA = new DefaultHttpContext(); contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason")); var contextB = new DefaultHttpContext(); contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason")); var contexts = new List { contextA, contextB, }; // Arrange: Given The Provider Returns Aggregator var aggregator = new TestDefinedAggregator(); _provider.Setup(x => x.Get(It.IsAny())).Returns(new OkResponse(aggregator)); // Act await _aggregator.Aggregate(route, context, contexts); // Assert _provider.Verify(x => x.Get(route), Times.Once); var content = await context.Items.DownstreamResponse().Content.ReadAsStringAsync(TestContext.Current.CancellationToken); content.ShouldBe("Tom, Laura"); } [Fact] public async Task Should_not_find_aggregator() { // Arrange var route = new Route(); var context = new DefaultHttpContext(); var contextA = new DefaultHttpContext(); contextA.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Tom"), HttpStatusCode.OK, new List>>(), "some reason")); var contextB = new DefaultHttpContext(); contextB.Items.UpsertDownstreamResponse(new DownstreamResponse(new StringContent("Laura"), HttpStatusCode.OK, new List>>(), "some reason")); var contexts = new List { contextA, contextB, }; // Arrange: Given The Provider Returns Error _provider.Setup(x => x.Get(It.IsAny())).Returns(new ErrorResponse(new AnyError())); // Act await _aggregator.Aggregate(route, context, contexts); // Assert _provider.Verify(x => x.Get(route), Times.Once); context.Items.Errors().Count.ShouldBeGreaterThan(0); context.Items.Errors().Count.ShouldBe(1); } public class TestDefinedAggregator : IDefinedAggregator { public async Task Aggregate(List responses) { var tom = await responses[0].Items.DownstreamResponse().Content.ReadAsStringAsync(); var laura = await responses[1].Items.DownstreamResponse().Content.ReadAsStringAsync(); var content = $"{tom}, {laura}"; var headers = responses.SelectMany(x => x.Items.DownstreamResponse().Headers).ToList(); return new DownstreamResponse(new StringContent(content), HttpStatusCode.OK, headers, "some reason"); } } } ================================================ FILE: test/Ocelot.UnitTests/Ocelot.UnitTests.csproj ================================================  0.0.0-dev net8.0;net9.0;net10.0 disable disable false true Ocelot.UnitTests Exe true win-x64;osx-x64 false false false ..\..\codeanalysis.ruleset True ..\..\codeanalysis.ruleset $(NoWarn);CS0618;CS1591 full True PreserveNewest PreserveNewest runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive all ================================================ FILE: test/Ocelot.UnitTests/Polly/CircuitBreakerStrategyTests.cs ================================================ using Ocelot.Provider.Polly; using Const = Ocelot.Provider.Polly.CircuitBreakerStrategy; namespace Ocelot.UnitTests.Polly; public class CircuitBreakerStrategyTests { [Theory] [Trait("PR", "2073")] [InlineData(0, Const.DefaultBreakDuration)] // out of range [InlineData(500, Const.DefaultBreakDuration)] // out of range [InlineData(501, 501)] // in range [InlineData(Const.DefaultBreakDuration, Const.DefaultBreakDuration)] // in range [InlineData(86_400_000 - 1, 86_400_000 - 1)] // in range [InlineData(86_400_000, Const.DefaultBreakDuration)] // out of range public void BreakDuration_ShouldBeInRange(int ms, int expected) { // Arrange, Act var actual = CircuitBreakerStrategy.BreakDuration(ms); // Assert Assert.Equal(expected, actual); } [Theory] [Trait("PR", "2073")] [InlineData(0, Const.DefaultMinimumThroughput)] // out of range [InlineData(1, Const.DefaultMinimumThroughput)] // out of range [InlineData(2, 2)] // in range [InlineData(Const.DefaultMinimumThroughput, Const.DefaultMinimumThroughput)] // in range public void MinimumThroughput_ShouldBeTwoOrGreater(int value, int expected) { // Arrange, Act var actual = CircuitBreakerStrategy.MinimumThroughput(value); // Assert Assert.Equal(expected, actual); } [Theory] [Trait("PR", "2081")] [Trait("Feat", "2080")] [InlineData(0.0D, Const.DefaultFailureRatio)] // out of range [InlineData(0.05D, 0.05D)] // in range [InlineData(Const.DefaultFailureRatio, Const.DefaultFailureRatio)] // in range [InlineData(0.99D, 0.99D)] // in range [InlineData(1.0, Const.DefaultFailureRatio)] // out of range public void FailureRatio_ShouldBeInRange(double ratio, double expected) { // Arrange, Act var actual = CircuitBreakerStrategy.FailureRatio(ratio); // Assert Assert.Equal(expected, actual); } [Theory] [Trait("PR", "2081")] [Trait("Feat", "2080")] [InlineData(0, Const.DefaultSamplingDuration)] // out of range [InlineData(500, Const.DefaultSamplingDuration)] // out of range [InlineData(501, 501)] // in range [InlineData(Const.DefaultSamplingDuration, Const.DefaultSamplingDuration)] // in range [InlineData(86_400_000 - 1, 86_400_000 - 1)] // in range [InlineData(86_400_000, Const.DefaultSamplingDuration)] // out of range public void SamplingDuration_ShouldBeInRange(int ms, int expected) { // Arrange, Act var actual = CircuitBreakerStrategy.SamplingDuration(ms); // Assert Assert.Equal(expected, actual); } } ================================================ FILE: test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Provider.Polly; using Ocelot.Provider.Polly.Interfaces; using Ocelot.QualityOfService; using Polly; namespace Ocelot.UnitTests.Polly; public class OcelotBuilderExtensionsTests { private readonly Mock _loggerFactory = new(); private readonly Mock _contextAccessor = new(); [Fact] public void DefaultErrorMapping_CallCreateRequestTimedOutError_IsTypeOfRequestTimedOutError() { foreach (var kv in OcelotBuilderExtensions.DefaultErrorMapping) { // Arrange var type = kv.Key; var mappingFunc = kv.Value; object[] args = type.IsGenericType ? [new HttpResponseMessage()] : []; var argument = (Exception)Activator.CreateInstance(type, args); // Act var actual = mappingFunc.Invoke(argument); // Assert Assert.IsType(actual); } } [Fact] public void AddPolly_NoParams_ShouldBuild() { // Arrange var provider = GivenServiceProvider( ob => ob.AddPolly(), out var route); // Act, Assert var del = provider.GetService().ShouldNotBeNull(); var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull(); handler.ShouldBeOfType(); } [Fact] public void AddPolly_GenericWithoutParams_ShouldBuild() { // Arrange var provider = GivenServiceProvider( ob => ob.AddPolly(), out var route); // Act, Assert var del = provider.GetService().ShouldNotBeNull(); var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull(); handler.ShouldBeOfType(); } [Fact] public void AddPolly_WithErrorMapping_ShouldBuild() { // Arrange var errorMapping = OcelotBuilderExtensions.DefaultErrorMapping; var provider = GivenServiceProvider( ob => ob.AddPolly(errorMapping), out var route); // Act, Assert var del = provider.GetService().ShouldNotBeNull(); var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull(); handler.ShouldBeOfType(); } [Fact] public void AddPolly_WithDelegatingHandler_ShouldBuild() { // Arrange var qosDelegatingHandler = new QosDelegatingHandlerDelegate(GetQosDelegatingHandler); var provider = GivenServiceProvider( ob => ob.AddPolly(qosDelegatingHandler), out var route); // Act, Assert var del = provider.GetService().ShouldNotBeNull(); var handler = del(route, _contextAccessor.Object, _loggerFactory.Object).ShouldNotBeNull(); handler.ShouldBeOfType(); } private static DelegatingHandler GetQosDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new MyQosDelegatingHandlerFor_AddPolly_WithDelegatingHandler_ShouldBuild(); private class MyQosDelegatingHandlerFor_AddPolly_WithDelegatingHandler_ShouldBuild : DelegatingHandler { protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => throw new Exception("Hello from the fake handler!"); } private class MyPollyQoSResiliencePipelineProvider : IPollyQoSResiliencePipelineProvider { public ResiliencePipeline GetResiliencePipeline(DownstreamRoute route) => throw new NotImplementedException(); } private static ServiceProvider GivenServiceProvider(Action withAddPolly, out DownstreamRoute route) { var services = new ServiceCollection(); var options = new QoSOptions(2, 200) { Timeout = 100, }; route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .Build(); var oBuilder = services.AddOcelot(configuration); withAddPolly(oBuilder); return services.BuildServiceProvider(true); } } ================================================ FILE: test/Ocelot.UnitTests/Polly/PollyQoSResiliencePipelineProviderTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; using Polly; using Polly.CircuitBreaker; using Polly.Registry; using Polly.Testing; using Polly.Timeout; using _TimeoutStrategy_ = Ocelot.Provider.Polly.TimeoutStrategy; namespace Ocelot.UnitTests.Polly; public class PollyQoSResiliencePipelineProviderTests { #region Constructor [Theory] [Trait("PR", "2073")] [InlineData(0)] [InlineData(1)] public void Ctor_NoLoggerParam_ShouldThrowArgumentNullException(int branch) { // Arrange IOcelotLoggerFactory factory = null; if (branch >= 0) factory = null; if (branch >= 1) factory = Mock.Of(); // Act var ex = Assert.Throws( () => new PollyQoSResiliencePipelineProvider(factory, null)); // Assert Assert.Equal("loggerFactory", ex.ParamName); } [Fact] [Trait("PR", "2073")] public void Ctor_NoRegistryParam_ShouldThrowArgumentNullException() { // Arrange var factory = new Mock(); factory.Setup(x => x.CreateLogger()) .Returns(Mock.Of()); ResiliencePipelineRegistry noRegistry = null; // !!! // Act var ex = Assert.Throws( () => new PollyQoSResiliencePipelineProvider(factory.Object, noRegistry)); // Assert Assert.Equal("registry", ex.ParamName); } #endregion [Fact] public void ShouldBuild() { // Arrange var options = new QoSOptions() { BreakDuration = CircuitBreakerStrategy.LowBreakDuration + 1, // 0.5s, minimum required by Polly MinimumThroughput = 2, // 2 is the minimum required by Polly Timeout = 1000, // 10ms, minimum required by Polly }; var route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); var provider = GivenProvider(); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); // Assert resiliencePipeline.ShouldNotBeNull(); resiliencePipeline.ShouldBeOfType>(); resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); } [Fact] [Trait("Bug", "2085")] public void ShouldNotBuild_ReturnedEmpty() { // Arrange var options = new QoSOptions(); // empty options var route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); var provider = GivenProvider(); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); // Assert resiliencePipeline.ShouldNotBeNull(); resiliencePipeline.ShouldBeOfType>(); resiliencePipeline.ShouldBe(ResiliencePipeline.Empty); } [Theory] [Trait("Bug", "2085")] [InlineData(CircuitBreakerStrategy.LowBreakDuration - 1, CircuitBreakerStrategy.DefaultBreakDuration)] // default [InlineData(CircuitBreakerStrategy.LowBreakDuration, CircuitBreakerStrategy.DefaultBreakDuration)] // default [InlineData(CircuitBreakerStrategy.LowBreakDuration + 1, CircuitBreakerStrategy.LowBreakDuration + 1)] // not default, exact public void ShouldBuild_WithDefaultBreakDuration(int durationOfBreak, int expectedMillisecons) { // Arrange var options = new QoSOptions() { BreakDuration = durationOfBreak, // 0.5s, minimum required by Polly MinimumThroughput = 2, // 2 is the minimum required by Polly Timeout = 1000, // 10ms, minimum required by Polly }; var route = new DownstreamRouteBuilder() .WithQosOptions(options) .Build(); var provider = GivenProvider(); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); // Assert resiliencePipeline.ShouldNotBeNull(); resiliencePipeline.ShouldBeOfType>(); resiliencePipeline.ShouldNotBe(ResiliencePipeline.Empty); var descriptor = resiliencePipeline.GetPipelineDescriptor(); descriptor.ShouldNotBeNull(); descriptor.Strategies.Count.ShouldBe(2); descriptor.Strategies[0].Options.ShouldBeOfType>(); descriptor.Strategies[1].Options.ShouldBeOfType(); var strategyOptions = descriptor.Strategies[0].Options as CircuitBreakerStrategyOptions; strategyOptions.ShouldNotBeNull(); strategyOptions.BreakDuration.ShouldBe(TimeSpan.FromMilliseconds(expectedMillisecons)); } [Fact] public void Should_return_same_circuit_breaker_for_given_route() { // Arrange var provider = GivenProvider(); var route1 = GivenDownstreamRoute("/"); var route2 = GivenDownstreamRoute("/"); // Act var resiliencePipeline1 = provider.GetResiliencePipeline(route1); var resiliencePipeline2 = provider.GetResiliencePipeline(route2); // Assert resiliencePipeline1.ShouldBe(resiliencePipeline2); // Act 2 var resiliencePipeline3 = provider.GetResiliencePipeline(route1); // Assert 2 resiliencePipeline3.ShouldBe(resiliencePipeline1); resiliencePipeline3.ShouldBe(resiliencePipeline2); } [Fact] public void Should_return_different_circuit_breaker_for_two_different_routes() { // Arrange var provider = GivenProvider(); var route1 = GivenDownstreamRoute("/"); var route2 = GivenDownstreamRoute("/test"); // Act var resiliencePipeline1 = provider.GetResiliencePipeline(route1); var resiliencePipeline2 = provider.GetResiliencePipeline(route2); // Assert resiliencePipeline1.ShouldNotBe(resiliencePipeline2); } [Fact] [Trait("Bug", "2085")] public void ShouldBuild_ContainsTwoStrategies() { var pollyQoSResiliencePipelineProvider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = pollyQoSResiliencePipelineProvider.GetResiliencePipeline(route); resiliencePipeline.ShouldNotBeNull(); var descriptor = resiliencePipeline.GetPipelineDescriptor(); descriptor.ShouldNotBeNull(); descriptor.Strategies.Count.ShouldBe(2); descriptor.Strategies[0].Options.ShouldBeOfType>(); descriptor.Strategies[1].Options.ShouldBeOfType(); } [Fact] public void Should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", 0); // get route with 0 exceptions allowed before breaking // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.GetPipelineDescriptor(); // Assert resiliencePipeline.ShouldNotBeNull(); descriptor.ShouldNotBeNull(); descriptor.Strategies.Count.ShouldBe(1); descriptor.Strategies.Single().Options.ShouldBeOfType(); } [Fact] [Trait("Bug", "2085")] public async Task Should_throw_after_timeout() { // Arrange var provider = GivenProvider(); const int OneSecond = 1000; var route = GivenDownstreamRoute("/", timeOut: OneSecond); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.OK); var cancellationTokenSource = new CancellationTokenSource(); // Assert await Assert.ThrowsAsync(async () => // Act await resiliencePipeline.ExecuteAsync(async (cancellationToken) => { await Task.Delay(OneSecond + 500, cancellationToken); // add 500ms to make sure it's timed out return response; }, cancellationTokenSource.Token)); } [Fact] [Trait("Bug", "2085")] public async Task Should_not_throw_before_timeout() { // Arrange var provider = GivenProvider(); const int OneSecond = 1000; var route = GivenDownstreamRoute("/", timeOut: OneSecond); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.OK); var cancellationTokenSource = new CancellationTokenSource(); // Act await resiliencePipeline.ExecuteAsync(async cancellationToken => { await Task.Delay(OneSecond - 100, cancellationToken); // subtract 100ms to make sure it's not timed out return response; }, cancellationTokenSource.Token); // Assert Assert.True(response.IsSuccessStatusCode); } [Theory] [InlineData(HttpStatusCode.InternalServerError)] [InlineData(HttpStatusCode.NotImplemented)] [InlineData(HttpStatusCode.BadGateway)] [InlineData(HttpStatusCode.ServiceUnavailable)] [InlineData(HttpStatusCode.GatewayTimeout)] [InlineData(HttpStatusCode.HttpVersionNotSupported)] [InlineData(HttpStatusCode.VariantAlsoNegotiates)] [InlineData(HttpStatusCode.InsufficientStorage)] [InlineData(HttpStatusCode.LoopDetected)] public async Task Should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(errorCode); // Act await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); } [Fact] public async Task Should_not_throw_broken_circuit_exception_if_status_code_ok() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.OK); // Act, Assert Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode); } [Fact] public async Task Should_throw_and_before_delay_should_not_allow_requests() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); await Task.Delay(200, TestContext.Current.CancellationToken); // Act, Assert 2 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); } [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); await Task.Delay(6000, TestContext.Current.CancellationToken); // Act 2 var response2 = await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Assert 2 Assert.Equal(HttpStatusCode.InternalServerError, response2.StatusCode); } [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); await Task.Delay(6000, TestContext.Current.CancellationToken); // Act, Assert 2 Assert.Equal(HttpStatusCode.InternalServerError, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)).StatusCode); // Act, Assert 3 await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); } [Fact] public async Task Should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/"); var resiliencePipeline = provider.GetResiliencePipeline(route); var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); // Act, Assert await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); await Task.Delay(10000, TestContext.Current.CancellationToken); // Act, Assert 2 var response2 = new HttpResponseMessage(HttpStatusCode.OK); Assert.Equal(HttpStatusCode.OK, (await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response2), TestContext.Current.CancellationToken)).StatusCode); // Act, Assert 3 await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken); await Assert.ThrowsAsync(async () => await resiliencePipeline.ExecuteAsync((_) => ValueTask.FromResult(response), TestContext.Current.CancellationToken)); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(null)] [InlineData(-1)] [InlineData(0)] public void ConfigureTimeout_NoQosTimeout_ShouldNotApplyTimeoutStrategy(int? timeout) { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", timeOut: timeout); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Single().Options.ShouldNotBeOfType(); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] public void ConfigureTimeout_HasInvalidTimeout_ShouldUseDefaultTimeout() { // Arrange int? invalidTimeout = _TimeoutStrategy_.LowTimeout - 1; var provider = GivenProvider(); var route = GivenDownstreamRoute("/", 0, invalidTimeout); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Single().Options.ShouldBeOfType(); var actual = descriptor.Strategies.Single().Options as TimeoutStrategyOptions; Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(null)] [InlineData(_TimeoutStrategy_.LowTimeout - 1)] public void ConfigureTimeout_ValidationIsAlwaysTrue_ShouldUseDefaultTimeout(int? invalidTimeout) { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", timeOut: invalidTimeout); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Count.ShouldBe(2); var strategy = descriptor.Strategies.SingleOrDefault(x => x.Options.GetType() == typeof(TimeoutStrategyOptions)).ShouldNotBeNull(); var actual = strategy.Options as TimeoutStrategyOptions; Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(null, "Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the timeout is disabled because the Timeout (?) is either undefined, negative, or zero.")] [InlineData(-1, "Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the timeout is disabled because the Timeout (-1) is either undefined, negative, or zero.")] public void IsConfigurationValidForTimeout_InvalidValue_ShouldLogError(int? invalidTimeout, string expectedMessage) { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", timeOut: invalidTimeout); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Single().Options.ShouldBeOfType>(); _logger.Verify(x => x.LogError(It.IsAny>(), It.IsAny()), Times.Once()); var message = _funcMessage?.Invoke() ?? string.Empty; message.ShouldBe(expectedMessage); } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] public void IsConfigurationValidForTimeout_ValidValueButIsNotValidTimeout_ShouldLogWarning() { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", timeOut: _TimeoutStrategy_.LowTimeout - 1); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Count.ShouldBe(2); var strategy = descriptor.Strategies.SingleOrDefault(x => x.Options.GetType() == typeof(TimeoutStrategyOptions)).ShouldNotBeNull(); var actual = strategy.Options as TimeoutStrategyOptions; Assert.Equal(_TimeoutStrategy_.DefaultTimeout, (int)actual.Timeout.TotalMilliseconds); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var message = _funcMessage?.Invoke() ?? string.Empty; message.ShouldBe("Route '/' has invalid QoSOptions for Polly's Timeout strategy. Specifically, the Timeout value (9) is outside the valid range (10 to 86400000 milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value (30000) will be substituted."); } [Fact] [Trait("PR", "2081")] [Trait("Feat", "2080")] public void The_ReturnedWithMessagePosition() { // Arrange 1 List> warnings = new(); static string msg1() => "A"; warnings.Add(msg1); // Act, Assert 1 PollyQoSResiliencePipelineProvider.The(warnings, msg1).ShouldBe("the"); // Arrange 2 static string msg2() => "B"; warnings.Add(msg2); // Act, Assert 2 var nl = Environment.NewLine; PollyQoSResiliencePipelineProvider.The(warnings, msg1).ShouldBe($"{nl} 1. The"); PollyQoSResiliencePipelineProvider.The(warnings, msg2).ShouldBe($"{nl} 2. The"); } [Theory] [Trait("PR", "2081")] [Trait("Feat", "2080")] [InlineData(null, "Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the circuit breaker is disabled because the MinimumThroughput value (?) is either undefined, negative, or zero.")] [InlineData(-1, "Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the circuit breaker is disabled because the MinimumThroughput value (-1) is either undefined, negative, or zero.")] public void IsConfigurationValidForCircuitBreaker_InvalidValue_ShouldLogError(int? exceptionsAllowedBeforeBreaking, string expectedMessage) { // Arrange var provider = GivenProvider(); var route = GivenDownstreamRoute("/", exceptionsAllowedBeforeBreaking, 555); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Single().Options.ShouldBeOfType(); _logger.Verify(x => x.LogError(It.IsAny>(), It.IsAny()), Times.Once()); var message = _funcMessage?.Invoke() ?? string.Empty; message.ShouldBe(expectedMessage); } [Fact] [Trait("PR", "2081")] [Trait("Feat", "2080")] public void IsConfigurationValidForCircuitBreaker_InvalidOptions_ShouldLogWarning() { // Arrange var provider = GivenProvider(); var invalidOptions = new QoSOptions() { MinimumThroughput = 1, // invalid BreakDuration = 0, FailureRatio = 0.0D, SamplingDuration = 0, Timeout = _TimeoutStrategy_.DefTimeout, // but timeout is valid }; var route = new DownstreamRouteBuilder() .WithQosOptions(invalidOptions) .WithUpstreamPathTemplate(new("/", 1, false, "/")) .Build(); // Act var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Count.ShouldBe(2); descriptor.Strategies.Single(x => x.Options.GetType() == typeof(CircuitBreakerStrategyOptions)); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var message = _funcMessage?.Invoke() ?? string.Empty; message.ShouldBe(@"Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, 1. The MinimumThroughput value (1) is less than the required LowMinimumThroughput threshold (2). Therefore, increase MinimumThroughput to at least 2 or higher. Until then, the default value (100) will be substituted. 2. The BreakDuration value (0) is outside the valid range (500 to 86400000 milliseconds). Therefore, ensure the value falls within this range; otherwise, the default value (5000) will be substituted. 3. The FailureRatio value (0) is outside the valid range (0 to 1). Therefore, ensure the ratio falls within this range; otherwise, the default value (0.1) will be substituted. 4. The SamplingDuration value (0) is outside the valid range (500 to 86400000 milliseconds). Therefore, ensure the duration falls within this range; otherwise, the default value (30000) will be substituted."); } [Fact] [Trait("PR", "2081")] [Trait("Feat", "2080")] public void IsConfigurationValidForCircuitBreaker_NullOptions_ShouldLogWarning() { // Arrange var provider = GivenProvider(); var nullOptions = new QoSOptions(1, null) // invalid { Timeout = _TimeoutStrategy_.DefTimeout, // but timeout is valid }; var route = new DownstreamRouteBuilder() .WithQosOptions(nullOptions) .WithUpstreamPathTemplate(new("/", 1, false, "/")) .Build(); // Act 2 var resiliencePipeline = provider.GetResiliencePipeline(route); var descriptor = resiliencePipeline.ShouldNotBeNull().GetPipelineDescriptor(); // Assert 2 descriptor.ShouldNotBeNull(); descriptor.Strategies.ShouldNotBeEmpty(); descriptor.Strategies.Count.ShouldBe(2); descriptor.Strategies.ShouldContain(x => x.Options.GetType() == typeof(CircuitBreakerStrategyOptions)); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var message = _funcMessage?.Invoke() ?? string.Empty; message.ShouldBe("Route '/' has invalid QoSOptions for Polly's Circuit Breaker strategy. Specifically, the MinimumThroughput value (1) is less than the required LowMinimumThroughput threshold (2). Therefore, increase MinimumThroughput to at least 2 or higher. Until then, the default value (100) will be substituted."); } private Func _funcMessage; private readonly Mock _logger = new(); private PollyQoSResiliencePipelineProvider GivenProvider() => GivenProvider(); private PollyQoSResiliencePipelineProvider GivenProvider() where T : PollyQoSResiliencePipelineProvider { _logger.Setup(x => x.LogError(It.IsAny>(), It.IsAny())) .Callback, Exception>((f, _) => _funcMessage = f); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>((f) => _funcMessage = f); var loggerFactory = new Mock(); loggerFactory.Setup(x => x.CreateLogger()) .Returns(_logger.Object); var registry = new ResiliencePipelineRegistry(); return (T)Activator.CreateInstance(typeof(T), loggerFactory.Object, registry); } private static DownstreamRoute GivenDownstreamRoute(string routeTemplate, int? exceptionsAllowedBeforeBreaking = 2, int? timeOut = 10000) { var options = new QoSOptions(exceptionsAllowedBeforeBreaking, 5000) { Timeout = timeOut, }; var upstreamPath = new UpstreamPathTemplateBuilder() .WithTemplate(routeTemplate) .WithContainsQueryString(false) .WithPriority(1) .WithOriginalValue(routeTemplate) .Build(); return new DownstreamRouteBuilder() .WithQosOptions(options) .WithUpstreamPathTemplate(upstreamPath) .WithLoadBalancerKey($"{routeTemplate}|no-host|localhost:20005,localhost:20007|no-svc-ns|no-svc-name|LeastConnection|no-lb-key") .Build(); } } internal class FakeTimeoutProvider : PollyQoSResiliencePipelineProvider { public FakeTimeoutProvider(IOcelotLoggerFactory loggerFactory, ResiliencePipelineRegistry registry) : base(loggerFactory, registry) { } protected override bool IsConfigurationValidForTimeout(DownstreamRoute route) => true; } ================================================ FILE: test/Ocelot.UnitTests/Polly/PollyResiliencePipelineDelegatingHandlerTests.cs ================================================ using Microsoft.AspNetCore.Http; using Moq.Protected; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; using Ocelot.Provider.Polly.Interfaces; using Polly; using Polly.Retry; using System.Reflection; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests.Polly; public class PollyResiliencePipelineDelegatingHandlerTests { private readonly Mock _innerHandler = new(); private readonly Mock _logger = new(); private readonly Mock> _pipelineProvider = new(); private readonly Mock _contextAccessor = new(); private readonly PollyResiliencePipelineDelegatingHandler _sut; private Func _loggerMessage; public PollyResiliencePipelineDelegatingHandlerTests() { var loggerFactory = new Mock(); loggerFactory.Setup(x => x.CreateLogger()) .Returns(_logger.Object); _logger.Setup(x => x.LogDebug(It.IsAny>())) .Callback>(f => _loggerMessage = f); _logger.Setup(x => x.LogInformation(It.IsAny>())) .Callback>(f => _loggerMessage = f); _sut = new PollyResiliencePipelineDelegatingHandler(DownstreamRouteFactory(), _contextAccessor.Object, loggerFactory.Object); } [Fact] public async Task SendAsync_WithPipeline_ExecutedByPipeline() { // Arrange var fakeResponse = GivenHttpResponseMessage(); SetupInnerHandler(fakeResponse); SetupResiliencePipelineProvider(); // Act var actual = await InvokeAsync("SendAsync"); // Assert ShouldHaveTestHeaderWithoutContent(actual); ShouldHaveCalledThePipelineProviderOnce(); #if DEBUG ShouldLogInformation("The Polly.ResiliencePipeline`1[System.Net.Http.HttpResponseMessage] pipeline has detected by QoS provider for the route with downstream URL ''. Going to execute request..."); #endif ShouldHaveCalledTheInnerHandlerOnce(); } [Fact] public async Task SendAsync_NoPipeline_SentWithoutPipeline() { // Arrange const bool PipelineIsNull = true; var fakeResponse = GivenHttpResponseMessage(); SetupInnerHandler(fakeResponse); SetupResiliencePipelineProvider(PipelineIsNull); // Act var actual = await InvokeAsync("SendAsync"); // Assert ShouldHaveTestHeaderWithoutContent(actual); ShouldHaveCalledThePipelineProviderOnce(); #if DEBUG ShouldLogDebug("No pipeline was detected by QoS provider for the route with downstream URL ''."); #endif ShouldHaveCalledTheInnerHandlerOnce(); } private void SetupInnerHandler(HttpResponseMessage fakeResponse) { _innerHandler.Protected() .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) .ReturnsAsync(fakeResponse); _sut.InnerHandler = _innerHandler.Object; } private void SetupResiliencePipelineProvider(bool pipelineIsNull = false) { var resiliencePipeline = new ResiliencePipelineBuilder() .AddRetry(new RetryStrategyOptions { ShouldHandle = new PredicateBuilder().Handle(), }) .Build(); _pipelineProvider.Setup(x => x.GetResiliencePipeline(It.IsAny())) .Returns(pipelineIsNull ? null : resiliencePipeline); var httpContext = new Mock(); httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSResiliencePipelineProvider))) .Returns(_pipelineProvider.Object); _contextAccessor.Setup(x => x.HttpContext) .Returns(httpContext.Object); } private async Task InvokeAsync(string methodName) { var m = typeof(PollyResiliencePipelineDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); var actual = await task!; return actual; } private static HttpResponseMessage GivenHttpResponseMessage([CallerMemberName] string headerValue = nameof(PollyResiliencePipelineDelegatingHandlerTests)) { var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); fakeResponse.Headers.Add("X-Xunit", headerValue); return fakeResponse; } private static void ShouldHaveTestHeaderWithoutContent(HttpResponseMessage actual, [CallerMemberName] string headerValue = nameof(PollyResiliencePipelineDelegatingHandlerTests)) { actual.ShouldNotBeNull(); actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); actual.Headers.GetValues("X-Xunit").ShouldContain(headerValue); } private void ShouldHaveCalledThePipelineProviderOnce() { _pipelineProvider.Verify(a => a.GetResiliencePipeline(It.IsAny()), Times.Once); _pipelineProvider.VerifyNoOtherCalls(); } private void ShouldHaveCalledTheInnerHandlerOnce() { _innerHandler.Protected().Verify>( "SendAsync", Times.Once(), ItExpr.IsAny(), ItExpr.IsAny()); } private void ShouldLogDebug(string expected) { _logger.Verify(x => x.LogDebug(It.IsAny>()), Times.Once); var msg = _loggerMessage.ShouldNotBeNull().Invoke(); msg.ShouldBe(expected); } private void ShouldLogInformation(string expected) { _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Once); var msg = _loggerMessage.ShouldNotBeNull().Invoke(); msg.ShouldBe(expected); } private static DownstreamRoute DownstreamRouteFactory() { var options = new QoSOptions(2, 200) { Timeout = 100, }; var upstreamPath = new UpstreamPathTemplateBuilder() .WithTemplate("/") .WithContainsQueryString(false) .WithPriority(1) .WithOriginalValue("/").Build(); return new DownstreamRouteBuilder() .WithQosOptions(options) .WithUpstreamPathTemplate(upstreamPath) .Build(); } } ================================================ FILE: test/Ocelot.UnitTests/Polly/TimeoutStrategyTests.cs ================================================ using Ocelot.Provider.Polly; using Const = Ocelot.Provider.Polly.TimeoutStrategy; namespace Ocelot.UnitTests.Polly; [Collection(nameof(SequentialTests))] public class TimeoutStrategyTests { [Theory] [Trait("PR", "2073")] [InlineData(0, Const.DefTimeout)] // out of range [InlineData(Const.LowTimeout - 1, Const.DefTimeout)] // out of range [InlineData(Const.LowTimeout, Const.DefTimeout)] // out of range [InlineData(Const.LowTimeout + 1, Const.LowTimeout + 1)] // in range [InlineData(Const.DefTimeout, Const.DefTimeout)] // in range [InlineData(Const.HighTimeout - 1, Const.HighTimeout - 1)] // in range [InlineData(Const.HighTimeout, Const.DefTimeout)] // out of range [InlineData(Const.HighTimeout + 1, Const.DefTimeout)] // out of range public void DefaultTimeout_Setter_ShouldBeGreaterThan10msAndLessThan24hours(int value, int expected) { // Arrange, Act TimeoutStrategy.DefaultTimeout = value; // Assert Assert.Equal(expected, TimeoutStrategy.DefaultTimeout); TimeoutStrategy.DefaultTimeout = TimeoutStrategy.DefTimeout; } } ================================================ FILE: test/Ocelot.UnitTests/Properties/AssemblyInfo.cs ================================================ using System.Reflection; using System.Runtime.InteropServices; // General Information about an assembly is controlled through the following set of attributes. // Change these attribute values to modify the information associated with an assembly. [assembly: AssemblyCompany("Three Mammals")] [assembly: AssemblyCopyright("© 2026 Three Mammals. MIT licensed OSS.")] [assembly: AssemblyProduct("Ocelot Gateway")] [assembly: AssemblyTrademark("Ocelot")] // Setting ComVisible to false makes the types in this assembly not visible to COM components. // If you need to access a type in this assembly from COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("54e84f1a-e525-4443-96ec-039cbd50c263")] ================================================ FILE: test/Ocelot.UnitTests/QualityOfService/FileGlobalQoSOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.QualityOfService; [Trait("Feat", "585")] [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 public class FileGlobalQoSOptionsTests { [Fact] public void Ctor() { // Arrange var actual = new FileGlobalQoSOptions(); // Assert AssertNullProps(actual); } [Fact] public void Ctor_FileQoSOptions() { // Arrange FileQoSOptions from = new() { DurationOfBreak = 1, BreakDuration = 2, ExceptionsAllowedBeforeBreaking = 3, MinimumThroughput = 4, FailureRatio = 5, SamplingDuration = 6, TimeoutValue = 7, Timeout = 8, }; // Act FileGlobalQoSOptions actual = new(from); // Assert Assert.NotSame(from, actual); Assert.Equivalent(from, actual); Assert.Null(actual.RouteKeys); } [Fact] public void Ctor_QoSOptions() { // Arrange QoSOptions from = new(2, 3) { FailureRatio = 4.0D, SamplingDuration = 5, Timeout = 6, }; // Act FileGlobalQoSOptions actual = new(from); // Assert Assert.NotSame(from, actual); Assert.Null(actual.RouteKeys); Assert.Equal(from.BreakDuration, actual.DurationOfBreak); Assert.Equal(from.BreakDuration, actual.BreakDuration); Assert.Equal(from.MinimumThroughput, actual.ExceptionsAllowedBeforeBreaking); Assert.Equal(from.MinimumThroughput, actual.MinimumThroughput); Assert.Equal(from.FailureRatio, actual.FailureRatio); Assert.Equal(from.SamplingDuration, actual.SamplingDuration); Assert.Equal(from.Timeout, actual.TimeoutValue); Assert.Equal(from.Timeout, actual.Timeout); } private static void AssertNullProps(FileGlobalQoSOptions actual) { Assert.NotNull(actual); Assert.Null(actual.RouteKeys); Assert.Null(actual.DurationOfBreak); Assert.Null(actual.BreakDuration); Assert.Null(actual.ExceptionsAllowedBeforeBreaking); Assert.Null(actual.MinimumThroughput); Assert.Null(actual.FailureRatio); Assert.Null(actual.SamplingDuration); Assert.Null(actual.TimeoutValue); Assert.Null(actual.Timeout); } } ================================================ FILE: test/Ocelot.UnitTests/QualityOfService/FileQoSOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.QualityOfService; public class FileQoSOptionsTests { [Fact] [Trait("PR", "2073")] [Trait("PR", "2081")] public void Ctor_Default_AllPropertiesAreNull() { // Arrange, Act var actual = new FileQoSOptions(); // Assert Assert.Null(actual.DurationOfBreak); Assert.Null(actual.BreakDuration); Assert.Null(actual.ExceptionsAllowedBeforeBreaking); Assert.Null(actual.MinimumThroughput); Assert.Null(actual.FailureRatio); Assert.Null(actual.SamplingDuration); Assert.Null(actual.TimeoutValue); Assert.Null(actual.Timeout); } [Fact] [Trait("PR", "2081")] [Trait("Feat", "2080")] public void Ctor_Copying_Copied() { // Arrange FileQoSOptions expected = new() { DurationOfBreak = 1, BreakDuration = 2, ExceptionsAllowedBeforeBreaking = 3, MinimumThroughput = 4, FailureRatio = 5.0D, SamplingDuration = 6, TimeoutValue = 7, Timeout = 8, }; // Act FileQoSOptions actual = new(expected); // copying // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } [Fact] [Trait("PR", "2081")] [Trait("Feat", "2080")] public void Ctor_CopyingQoSOptions_Copied() { // Arrange FileQoSOptions expected = new() { DurationOfBreak = 3, BreakDuration = 3, ExceptionsAllowedBeforeBreaking = 2, MinimumThroughput = 2, FailureRatio = 4.0D, SamplingDuration = 5, TimeoutValue = 6, Timeout = 6, }; QoSOptions from = new(2, 3) { FailureRatio = 4.0D, SamplingDuration = 5, Timeout = 6, }; // Act FileQoSOptions actual = new(from); // copying // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } private static void AssertEquality(FileQoSOptions actual, FileQoSOptions expected) { Assert.Equal(expected.DurationOfBreak, actual.DurationOfBreak); Assert.Equal(expected.BreakDuration, actual.BreakDuration); Assert.Equal(expected.ExceptionsAllowedBeforeBreaking, actual.ExceptionsAllowedBeforeBreaking); Assert.Equal(expected.MinimumThroughput, actual.MinimumThroughput); Assert.Equal(expected.FailureRatio, actual.FailureRatio); Assert.Equal(expected.SamplingDuration, actual.SamplingDuration); Assert.Equal(expected.TimeoutValue, actual.TimeoutValue); Assert.Equal(expected.Timeout, actual.Timeout); } } ================================================ FILE: test/Ocelot.UnitTests/QualityOfService/QoSFactoryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.QualityOfService; namespace Ocelot.UnitTests.QualityOfService; public class QoSFactoryTests { private QoSFactory _factory; private ServiceCollection _services; private readonly Mock _loggerFactory; private readonly Mock _contextAccessor; public QoSFactoryTests() { _services = new ServiceCollection(); _loggerFactory = new Mock(); _contextAccessor = new Mock(); var provider = _services.BuildServiceProvider(true); _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); } [Fact] public void Should_return_error() { // Arrange var downstreamRoute = new DownstreamRouteBuilder().Build(); // Act var handler = _factory.Get(downstreamRoute); // Assert handler.IsError.ShouldBeTrue(); handler.Errors[0].ShouldBeOfType(); } [Fact] public void Should_return_handler() { // Arrange _services = new ServiceCollection(); static DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); _services.AddSingleton(QosDelegatingHandlerDelegate); var provider = _services.BuildServiceProvider(true); _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); var downstreamRoute = new DownstreamRouteBuilder().Build(); // Act var handler = _factory.Get(downstreamRoute); // Assert handler.IsError.ShouldBeFalse(); handler.Data.ShouldBeOfType(); } private class FakeDelegatingHandler : DelegatingHandler { } } ================================================ FILE: test/Ocelot.UnitTests/QualityOfService/QoSOptionsCreatorTests.cs ================================================ using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Reflection; namespace Ocelot.UnitTests.QualityOfService; [Trait("Feat", "23")] // https://github.com/ThreeMammals/Ocelot/issues/23 [Trait("Release", "1.3.2")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.3.2 [Trait("Commit", "b44c025")] // https://github.com/ThreeMammals/Ocelot/commit/b44c02510af9904a5253ab0a3e6f1f6be9cd8aeb public class QoSOptionsCreatorTests : UnitTest { private readonly QoSOptionsCreator _creator = new(); [Fact] public void ShouldCreateQosOptions() { // Arrange var route = new FileRoute { QoSOptions = new FileQoSOptions { DurationOfBreak = 1, ExceptionsAllowedBeforeBreaking = 2, FailureRatio = 3.0D, SamplingDuration = 4, TimeoutValue = 5, }, }; var expected = new QoSOptions(2, 1) { FailureRatio = 3.0D, SamplingDuration = 4, Timeout = 5, }; // Act var actual = _creator.Create(route.QoSOptions); // Assert AssertEquality(actual, expected); } #region PR 2081 [Fact] [Trait("PR", "2081")] // https://github.com/ThreeMammals/Ocelot/pull/2081 [Trait("Feat", "2080")] // https://github.com/ThreeMammals/Ocelot/issues/2080 public void NoRouteOptions_ShouldCreateFromGlobalQosOptions() { // Arrange FileGlobalConfiguration global = new() { QoSOptions = new() { DurationOfBreak = 1, ExceptionsAllowedBeforeBreaking = 2, FailureRatio = 3.0D, SamplingDuration = 4, TimeoutValue = 5, }, }; FileRoute route = new(); QoSOptions expected = new(global.QoSOptions); // Act var actual = _creator.Create(route, global); // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } [Fact] [Trait("PR", "2081")] // https://github.com/ThreeMammals/Ocelot/pull/2081 [Trait("Feat", "2080")] // https://github.com/ThreeMammals/Ocelot/issues/2080 public void HasRouteOptions_ShouldCreateFromRouteQosOptions() { // Arrange FileGlobalConfiguration global = new() { QoSOptions = new() { DurationOfBreak = 1, ExceptionsAllowedBeforeBreaking = 2, FailureRatio = 3.0D, SamplingDuration = 4, TimeoutValue = 5, }, }; FileRoute route = new() { QoSOptions = new FileQoSOptions { DurationOfBreak = 10, ExceptionsAllowedBeforeBreaking = 20, FailureRatio = 30.0D, SamplingDuration = 40, TimeoutValue = 50, }, }; QoSOptions expected = new(route.QoSOptions); // Act var actual = _creator.Create(route, global); // Assert Assert.Equivalent(expected, actual); AssertEquality(actual, expected); } private static void AssertEquality(QoSOptions actual, QoSOptions expected) { Assert.Equal(expected.BreakDuration, actual.BreakDuration); Assert.Equal(expected.MinimumThroughput, actual.MinimumThroughput); Assert.Equal(expected.FailureRatio, actual.FailureRatio); Assert.Equal(expected.SamplingDuration, actual.SamplingDuration); Assert.Equal(expected.Timeout, actual.Timeout); } #endregion PR 2081 #region PR 2339 [Fact] [Trait("PR", "2339")] // https://github.com/ThreeMammals/Ocelot/pull/2339 [Trait("Feat", "2338")] // https://github.com/ThreeMammals/Ocelot/issues/2338 public void Create_FileQoSOptions() { // Arrange FileQoSOptions options = new() { DurationOfBreak = 1, BreakDuration = 2, ExceptionsAllowedBeforeBreaking = 3, MinimumThroughput = 4, FailureRatio = 5, SamplingDuration = 6, TimeoutValue = 7, Timeout = 8, }; // Act var actual = _creator.Create(options); // Assert Assert.NotNull(actual); Assert.Equal(1, actual.BreakDuration); Assert.Equal(3, actual.MinimumThroughput); Assert.Equal(5, actual.FailureRatio); Assert.Equal(6, actual.SamplingDuration); Assert.Equal(7, actual.Timeout); // Scenario 2: Create from null options = null; actual = _creator.Create(options); Assert.NotNull(actual); Assert.Null(actual.MinimumThroughput); Assert.Null(actual.Timeout); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_FileRoute_ArgNullChecks() { // Arrange FileRoute route = null; FileGlobalConfiguration globalConfiguration = null; // Act, Assert var ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), ex.ParamName); // Act, Assert route = new(); ex = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), ex.ParamName); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_FileRoute() { // Arrange FileRoute route = new() { QoSOptions = new() { DurationOfBreak = 1, BreakDuration = 1, ExceptionsAllowedBeforeBreaking = 1, MinimumThroughput = 1, FailureRatio = null, SamplingDuration = null, TimeoutValue = 1, Timeout = 1, }, }; FileGlobalConfiguration globalConfiguration = new() { QoSOptions = new() { DurationOfBreak = 3, BreakDuration = 3, ExceptionsAllowedBeforeBreaking = 3, MinimumThroughput = 3, FailureRatio = 3, SamplingDuration = 3, TimeoutValue = 3, Timeout = 3, }, }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.Equal(1, actual.BreakDuration); Assert.Equal(1, actual.MinimumThroughput); Assert.Equal(3, actual.FailureRatio); // global Assert.Equal(3, actual.SamplingDuration); // global Assert.Equal(1, actual.Timeout); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_FileDynamicRoute_ArgNullChecks() { // Arrange, Act, Assert FileDynamicRoute route = null; FileGlobalConfiguration globalConfiguration = null; var actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(route), actual.ParamName); // Arrange, Act, Assert 2 route = new(); globalConfiguration = null; actual = Assert.Throws(() => _creator.Create(route, globalConfiguration)); Assert.Equal(nameof(globalConfiguration), actual.ParamName); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_FileDynamicRoute() { // Arrange FileDynamicRoute route = new() { QoSOptions = new() { DurationOfBreak = 1, BreakDuration = 1, ExceptionsAllowedBeforeBreaking = 1, MinimumThroughput = 1, FailureRatio = null, SamplingDuration = null, TimeoutValue = 1, Timeout = 1, }, }; FileGlobalConfiguration globalConfiguration = new() { QoSOptions = new() { DurationOfBreak = 3, BreakDuration = 3, ExceptionsAllowedBeforeBreaking = 3, MinimumThroughput = 3, FailureRatio = 3, SamplingDuration = 3, TimeoutValue = 3, Timeout = 3, }, }; // Act var actual = _creator.Create(route, globalConfiguration); // Assert Assert.Equal(1, actual.BreakDuration); Assert.Equal(1, actual.MinimumThroughput); Assert.Equal(3, actual.FailureRatio); // global Assert.Equal(3, actual.SamplingDuration); // global Assert.Equal(1, actual.Timeout); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_IRouteGrouping_NullCheck() { // Arrange var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic); IRouteGrouping grouping = null; FileQoSOptions options = null; FileGlobalQoSOptions globalOptions = null; // Act var wrapper = Assert.Throws( () => method.Invoke(_creator, [grouping, options, globalOptions])); // Assert Assert.IsType(wrapper.InnerException); var actual = (ArgumentNullException)wrapper.InnerException; Assert.Equal(nameof(grouping), actual.ParamName); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create() // protected { // Scenario 1: Null check Create_IRouteGrouping_NullCheck(); var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic); const int global = 3, route = 1; // Scenario 2: if branches FileDynamicRoute grouping = new() { Key = "r1" }; FileQoSOptions options = null; FileGlobalQoSOptions globalOptions = new() { RouteKeys = null, DurationOfBreak = global, BreakDuration = global, ExceptionsAllowedBeforeBreaking = global, MinimumThroughput = global, FailureRatio = global, SamplingDuration = global, TimeoutValue = global, Timeout = global, }; // Act, Assert : from global opts var actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]); Assert.Equal(global, actual.BreakDuration); Assert.Equal(global, actual.MinimumThroughput); Assert.Equal(global, actual.FailureRatio); Assert.Equal(global, actual.SamplingDuration); Assert.Equal(global, actual.Timeout); // Arrange 2 options = new() { DurationOfBreak = route, BreakDuration = route, ExceptionsAllowedBeforeBreaking = route, MinimumThroughput = route, FailureRatio = route, SamplingDuration = route, TimeoutValue = route, Timeout = route, }; globalOptions.RouteKeys = ["?"]; // Act, Assert 2 : from route actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]); Assert.Equal(route, actual.BreakDuration); Assert.Equal(route, actual.MinimumThroughput); Assert.Equal(route, actual.FailureRatio); Assert.Equal(route, actual.SamplingDuration); Assert.Equal(route, actual.Timeout); globalOptions.RouteKeys = [grouping.Key]; actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]); Assert.Equal(route, actual.BreakDuration); Assert.Equal(route, actual.MinimumThroughput); Assert.Equal(route, actual.FailureRatio); Assert.Equal(route, actual.SamplingDuration); Assert.Equal(route, actual.Timeout); globalOptions = null; actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]); Assert.Equal(route, actual.BreakDuration); Assert.Equal(route, actual.MinimumThroughput); Assert.Equal(route, actual.FailureRatio); Assert.Equal(route, actual.SamplingDuration); Assert.Equal(route, actual.Timeout); // Arrange 3 : Merging options.FailureRatio = null; options.SamplingDuration = null; globalOptions = new() { RouteKeys = null, DurationOfBreak = global, BreakDuration = global, ExceptionsAllowedBeforeBreaking = global, MinimumThroughput = global, FailureRatio = global, SamplingDuration = global, TimeoutValue = global, Timeout = global, }; actual = (QoSOptions)method.Invoke(_creator, [grouping, options, globalOptions]); Assert.Equal(route, actual.BreakDuration); Assert.Equal(route, actual.MinimumThroughput); Assert.Equal(global, actual.FailureRatio); Assert.Equal(global, actual.SamplingDuration); Assert.Equal(route, actual.Timeout); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Create_IRouteGrouping_NoOptions() { // Arrange var method = _creator.GetType().GetMethod(nameof(Create), BindingFlags.Instance | BindingFlags.NonPublic); FileDynamicRoute route = new(); FileQoSOptions options = null; FileGlobalQoSOptions globalOptions = null; // Act var actual = (QoSOptions)method.Invoke(_creator, [route, options, globalOptions]); // Assert Assert.NotNull(actual); Assert.Null(actual.BreakDuration); Assert.Null(actual.MinimumThroughput); Assert.Null(actual.FailureRatio); Assert.Null(actual.SamplingDuration); Assert.Null(actual.Timeout); } [Fact] [Trait("PR", "2339")] [Trait("Feat", "2338")] public void Merge() { // Arrange null args var method = _creator.GetType().GetMethod(nameof(Merge), BindingFlags.Instance | BindingFlags.NonPublic); FileQoSOptions options = null, global = null; // Act var actual = (QoSOptions)method.Invoke(_creator, [options, global]); // Assert Assert.NotNull(actual); Assert.Null(actual.BreakDuration); Assert.Null(actual.MinimumThroughput); Assert.Null(actual.FailureRatio); Assert.Null(actual.SamplingDuration); Assert.Null(actual.Timeout); } #endregion PR 2339 } ================================================ FILE: test/Ocelot.UnitTests/QualityOfService/QoSOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.QualityOfService; public class QoSOptionsTests { [Fact] public void Ctor_Copy_ShouldCopy() { // Arrange var copyee = new QoSOptions(1, 2) { FailureRatio = 3.0D, SamplingDuration = 4, Timeout = 5, }; // Act var actual = new QoSOptions(copyee); // Assert Assert.Equivalent(copyee, actual); Assert.Equal(copyee.MinimumThroughput, actual.MinimumThroughput); Assert.Equal(copyee.BreakDuration, actual.BreakDuration); Assert.Equal(copyee.Timeout, actual.Timeout); Assert.Equal(copyee.FailureRatio, actual.FailureRatio); Assert.Equal(copyee.SamplingDuration, actual.SamplingDuration); } [Fact] [Trait("PR", "2073")] public void UseQos_NoOptions_ShouldNotUse() { // Arrange var from = new FileQoSOptions(); var opts = new QoSOptions(from); // Act, Assert Assert.False(opts.UseQos); } [Theory] [Trait("PR", "2073")] [InlineData(0, false)] // should not use [InlineData(1, true)] // should use public void UseQos_ExceptionsAllowedBeforeBreaking_ShouldUse(int exceptionsAllowed, bool expected) { // Arrange var opts = new QoSOptions() { MinimumThroughput = exceptionsAllowed, }; // timeoutValue is null // Act, Assert Assert.Equal(expected, opts.UseQos); } [Theory] [Trait("PR", "2073")] [InlineData(null, false)] // should not use [InlineData(0, false)] // should not use [InlineData(1, true)] // should use public void UseQos_TimeoutValue_ShouldUse(int? timeout, bool expected) { // Arrange var opts = new QoSOptions(timeout); // no exceptionsAllowedBeforeBreaking // Act, Assert Assert.Equal(expected, opts.UseQos); } } ================================================ FILE: test/Ocelot.UnitTests/QueryStrings/AddQueriesToRequestTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Errors; using Ocelot.Infrastructure.Claims; using Ocelot.QueryStrings; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.QueryStrings; /// /// Feature: Claims to Query String Parameters. /// [Trait("Commit", "f7f4a39")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93 [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class AddQueriesToRequestTests : UnitTest { private readonly AddQueriesToRequest _addQueriesToRequest; private DownstreamRequest _downstreamRequest; private readonly Mock _parser; private HttpRequestMessage _request; public AddQueriesToRequestTests() { _request = new HttpRequestMessage(HttpMethod.Post, "http://my.url/abc?q=123"); _parser = new Mock(); _addQueriesToRequest = new AddQueriesToRequest(_parser.Object); _downstreamRequest = new DownstreamRequest(_request); } [Fact] public void Should_add_new_queries_to_downstream_request() { // Arrange var claims = new List { new("test", "data"), }; var configuration = new List { new("query-key", string.Empty, string.Empty, 0), }; var claimValue = GivenTheClaimParserReturns(new OkResponse("value")); // Act var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeFalse(); ThenTheQueryIsAdded(claimValue); } [Fact] public void Should_add_new_queries_to_downstream_request_and_preserve_other_queries() { // Arrange var claims = new List { new("test", "data"), }; var configuration = new List { new("query-key", string.Empty, string.Empty, 0), }; GivenTheDownstreamRequestHasQueryString("?test=1&test=2"); var claimValue = GivenTheClaimParserReturns(new OkResponse("value")); // Act var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeFalse(); ThenTheQueryIsAdded(claimValue); TheTheQueryStringIs("?test=1&test=2&query-key=value"); } private void TheTheQueryStringIs(string expected) { _downstreamRequest.Query.ShouldBe(expected); } [Fact] public void Should_replace_existing_queries_on_downstream_request() { // Arrange var claims = new List { new("test", "data"), }; var configuration = new List { new("query-key", string.Empty, string.Empty, 0), }; GivenTheDownstreamRequestHasQueryString("query-key", "initial"); var claimValue = GivenTheClaimParserReturns(new OkResponse("value")); // Act var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeFalse(); ThenTheQueryIsAdded(claimValue); } [Fact] public void Should_return_error() { // Arrange var claims = new List(); var configuration = new List { new(string.Empty, string.Empty, string.Empty, 0), }; _ = GivenTheClaimParserReturns(new ErrorResponse(new List { new AnyError(), })); // Act var result = _addQueriesToRequest.SetQueriesOnDownstreamRequest(configuration, claims, _downstreamRequest); // Assert result.IsError.ShouldBeTrue(); } private void ThenTheQueryIsAdded(Response claimValue) { var queries = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString); var query = queries.First(x => x.Key == "query-key"); query.Value.First().ShouldBe(claimValue.Data); } private void GivenTheDownstreamRequestHasQueryString(string queryString) { _request = new HttpRequestMessage(HttpMethod.Post, $"http://my.url/abc{queryString}"); _downstreamRequest = new DownstreamRequest(_request); } private void GivenTheDownstreamRequestHasQueryString(string key, string value) { var newUri = Microsoft.AspNetCore.WebUtilities.QueryHelpers .AddQueryString(_downstreamRequest.ToHttpRequestMessage().RequestUri.OriginalString, key, value); _request.RequestUri = new Uri(newUri); } private Response GivenTheClaimParserReturns(Response claimValue) { _parser.Setup(x => x.GetValue(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(claimValue); return claimValue; } private class AnyError : Error { public AnyError() : base("blahh", OcelotErrorCode.UnknownError, 404) { } } } ================================================ FILE: test/Ocelot.UnitTests/QueryStrings/ClaimsToQueryStringMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.QueryStrings; using Ocelot.Request.Middleware; using Ocelot.Responses; using System.Security.Claims; namespace Ocelot.UnitTests.QueryStrings; /// /// Feature: Claims to Query String Parameters. /// [Trait("Commit", "f7f4a39")] // https://github.com/ThreeMammals/Ocelot/commit/f7f4a392f0743b38cd0206a81b4c094e60fe7b93 [Trait("Release", "1.1.0")] // https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0-beta.1 -> https://github.com/ThreeMammals/Ocelot/releases/tag/1.1.0 public class ClaimsToQueryStringMiddlewareTests : UnitTest { private readonly Mock _addQueries; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ClaimsToQueryStringMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ClaimsToQueryStringMiddlewareTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _addQueries = new Mock(); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); _middleware = new ClaimsToQueryStringMiddleware(_next, _loggerFactory.Object, _addQueries.Object); } [Fact] public async Task Should_call_add_queries_correctly() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithClaimsToQueries(new List { new("UserId", "Subject", string.Empty, 0), }) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); var downstreamRoute = new DownstreamRouteHolder( new(), new Route(route, HttpMethod.Get)); _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); _addQueries.Setup(x => x.SetQueriesOnDownstreamRequest(It.IsAny>(), It.IsAny>(), It.IsAny())) .Returns(new OkResponse()); // Act await _middleware.Invoke(_httpContext); // Assert _addQueries.Verify(x => x.SetQueriesOnDownstreamRequest(It.IsAny>(), It.IsAny>(), _httpContext.Items.DownstreamRequest()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/DistributedCacheRateLimitStorageTests.cs ================================================ using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; using Ocelot.RateLimiting; using System.Text; namespace Ocelot.UnitTests.RateLimiting; public class DistributedCacheRateLimitStorageTests { private readonly Mock _cache; private readonly DistributedCacheRateLimitStorage _storage; public DistributedCacheRateLimitStorageTests() { _cache = new Mock(); _storage = new DistributedCacheRateLimitStorage(_cache.Object); } [Fact] public void Set_ShouldSerializeAndStoreValue() { // Arrange var id = "test-id"; var counter = new RateLimitCounter(DateTime.UtcNow, null, 5); var expiration = TimeSpan.FromMinutes(1); var expectedJson = JsonConvert.SerializeObject(counter); var expectedBytes = Encoding.UTF8.GetBytes(expectedJson); _cache.Setup(c => c.Set(id, It.IsAny(), It.IsAny())); // Act _storage.Set(id, counter, expiration); // Assert _cache.Verify(c => c.Set(id, expectedBytes, It.Is(opt => opt.AbsoluteExpirationRelativeToNow == expiration)), Times.Once); } [Theory] [InlineData("", false)] [InlineData("{\"StartedAt\":\"2025-09-11T12:00:00Z\",\"Total\":1}", true)] public void Exists_ShouldReturnCorrectBoolean(string storedValue, bool expected) { // Arrange var id = "exists-id"; var bytes = Encoding.UTF8.GetBytes(storedValue); _cache.Setup(c => c.Get(id)).Returns(bytes); // Act var actual = _storage.Exists(id); // Assert _cache.Verify(c => c.Get(id), Times.Once); Assert.Equal(expected, actual); } [Fact] public void Get_ShouldDeserializeStoredValue() { // Arrange var id = "get-id"; var counter = new RateLimitCounter(DateTime.UtcNow, null, 3); var json = JsonConvert.SerializeObject(counter); var bytes = Encoding.UTF8.GetBytes(json); _cache.Setup(c => c.Get(id)).Returns(bytes); // Act var actual = _storage.Get(id); // Assert _cache.Verify(c => c.Get(id), Times.Once); Assert.NotNull(actual); Assert.Equal(counter.StartedAt, actual.Value.StartedAt); Assert.Equal(counter.Total, actual.Value.Total); } [Fact] public void Get_ShouldReturnNull_WhenStoredValueIsNullOrEmpty() { // Arrange var id = "null-id"; var json = string.Empty; var bytes = Encoding.UTF8.GetBytes(json); _cache.Setup(c => c.Get(id)).Returns(bytes); // Act var actual = _storage.Get(id); // Assert _cache.Verify(c => c.Get(id), Times.Once); Assert.Null(actual); } [Fact] public void Remove_ShouldCallCacheRemove() { // Arrange var id = "remove-id"; // Act _storage.Remove(id); // Assert _cache.Verify(c => c.Remove(id), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/FileGlobalRateLimitingTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.RateLimiting; public class FileGlobalRateLimitingTests { [Fact] public void FileGlobalRateLimit_Ctor() { // Arrange, Act FileGlobalRateLimit actual = new(); // Assert Assert.Null(actual.Name); Assert.Null(actual.Pattern); } [Fact] public void FileGlobalRateLimitByAspNetRule_Ctor() { // Arrange, Act FileGlobalRateLimitByAspNetRule actual = new(); // Assert Assert.Null(actual.RouteKeys); } [Fact] public void FileGlobalRateLimitByHeaderRule_Ctor() { // Arrange, Act, Assert FileGlobalRateLimitByHeaderRule actual = new(); Assert.Null(actual.RouteKeys); // Arrange FileRateLimitByHeaderRule from = new() { ClientIdHeader = "1", ClientWhitelist = ["2"], DisableRateLimitHeaders = true, HttpStatusCode = 4, QuotaExceededMessage = "5", RateLimitCounterPrefix = "6", }; // Act FileGlobalRateLimitByHeaderRule actualG = new(from); // Assert Assert.False(ReferenceEquals(from, actualG)); Assert.Equivalent(from, actualG); } [Fact] public void FileGlobalRateLimitByIpRule_Ctor() { // Arrange, Act FileGlobalRateLimitByIpRule actual = new(); // Assert Assert.Null(actual.RouteKeys); } [Fact] public void FileGlobalRateLimitByMethodRule_Ctor() { // Arrange, Act FileGlobalRateLimitByMethodRule actual = new(); // Assert Assert.Null(actual.RouteKeys); } [Fact] public void FileGlobalRateLimiting_Ctor() { // Arrange, Act FileGlobalRateLimiting actual = new(); // Assert Assert.Null(actual.ByHeader); Assert.Null(actual.ByMethod); Assert.Null(actual.ByIP); Assert.Null(actual.ByAspNet); Assert.Null(actual.Metadata); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/FileRateLimitByHeaderRuleTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.RateLimiting; public class FileRateLimitByHeaderRuleTests { [Fact] public void Ctor_Parameterless() { // Arrange, Act FileRateLimitByHeaderRule actual = new(); // Assert Assert.Null(actual.ClientIdHeader); Assert.Null(actual.ClientWhitelist); } [Fact] public void Ctor_CopyingFrom_FileRateLimitRule() { // Arrange FileRateLimitRule from = new() { EnableHeaders = false, EnableRateLimiting = true, KeyPrefix = "3", Limit = 4, Period = "5", PeriodTimespan = 6D, QuotaMessage = "7", StatusCode = 8, Wait = "9", }; // Act FileRateLimitByHeaderRule actual = new(from); FileRateLimitRule actualRule = actual; // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actualRule); Assert.Null(actual.ClientWhitelist); } [Fact] public void Ctor_CopyingFrom_FileRateLimitByHeaderRule() { // Arrange FileRateLimitByHeaderRule from = new() { ClientIdHeader = "1", ClientWhitelist = ["2"], DisableRateLimitHeaders = true, HttpStatusCode = 4, QuotaExceededMessage = "5", RateLimitCounterPrefix = "6", }; // Act FileRateLimitByHeaderRule actual = new(from); // Assert Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); } [Fact] public void ToString_DisabledRateLimiting_ShouldBeEmpty() { // Arrange FileRateLimitByHeaderRule rule = new() { EnableRateLimiting = false, }; // Act var actual = rule.ToString(); // Assert Assert.Empty(actual); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, true, "H+:3:2s:w1s/HDR:hdr/WL[client1]")] [InlineData(false, true, "H+:3:2s:w1s/HDR:hdr/WL[client1]")] [InlineData(true, true, "H-:3:2s:w1s/HDR:hdr/WL[client1]")] [InlineData(null, false, "H-:3:2s:w1s/HDR:hdr/WL[client1]")] [InlineData(false, false, "H+:3:2s:w1s/HDR:hdr/WL[client1]")] [InlineData(true, false, "H-:3:2s:w1s/HDR:hdr/WL[client1]")] public void ToString_DisableRateLimitHeaders(bool? disableRateLimitHeaders, bool enableHeadersstring, string expected) { // Format: H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]. // Arrange var rule = GivenRule(); rule.EnableHeaders = enableHeadersstring; rule.DisableRateLimitHeaders = disableRateLimitHeaders; // Act var actual = rule.ToString(); // Assert Assert.Equal(expected, actual); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, "H+:3:2s:w1s/HDR:hdr/WL-")] [InlineData(true, "H+:3:2s:w1s/HDR:hdr/WL[]")] [InlineData(false, "H+:3:2s:w1s/HDR:hdr/WL[cl1,cl2]")] public void ToString_ClientWhitelist(bool? isEmpty, string expected) { // Format: H{+,-}:{limit}:{period}:w{wait}/HDR:{client_id_header}/WL[{c1,c2,...}]. // Arrange var rule = GivenRule(); rule.ClientWhitelist = isEmpty switch { null => null, true => [], false => ["cl1", "cl2"], }; // Act var actual = rule.ToString(); // Assert Assert.Equal(expected, actual); } private static FileRateLimitByHeaderRule GivenRule() => new() { Limit = 3, Period = "2s", Wait = "1s", ClientIdHeader = "hdr", ClientWhitelist = ["client1"], }; } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/FileRateLimitRuleTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.RateLimiting; public class FileRateLimitRuleTests { [Fact] public void Ctor_Copying_ArgCheck() { // Arrange, Act var ex = Assert.Throws(() => new FileRateLimitRule(null)); // Assert Assert.Equal(ex.ParamName, "from"); } [Fact] public void Ctor_Copying_Copied() { // Arrange FileRateLimitRule from = new() { EnableRateLimiting = true, EnableHeaders = true, Limit = 3, Period = "4s", PeriodTimespan = 5D, Wait = "6s", StatusCode = 7, QuotaMessage = "8", KeyPrefix = "9", }; // Act var actual = new FileRateLimitRule(from); // Assert Assert.NotNull(actual); Assert.False(ReferenceEquals(from, actual)); Assert.Equivalent(from, actual); } [Fact] [Trait("Feat", "1229")] // https://github.com/ThreeMammals/Ocelot/issues/1229 [Trait("PR", "2294")] // https://github.com/ThreeMammals/Ocelot/pull/2294 public void ToString_DisabledRateLimiting_ShouldBeEmpty() { // Arrange FileRateLimitRule rule = new() { EnableRateLimiting = false, }; // Act var actual = rule.ToString(); // Assert Assert.Empty(actual); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void ToString_HappyPath() { // Arrange FileRateLimitRule rule = new() { Limit = 3, Period = "1s", Wait = "2s", }; // Act var actual = rule.ToString(); // Assert Assert.Equal("H+:3:1s:w2s", actual); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, "H+:::w-")] [InlineData(true, "H+:::w-")] [InlineData(false, "H-:::w-")] public void ToString_EnableHeaders(bool? enableHeaders, string expected) { // Arrange FileRateLimitRule rule = new() { EnableHeaders = enableHeaders, }; // Act var actual = rule.ToString(); // Assert Assert.Equal(expected, actual); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, "H+:::w-")] [InlineData(1.234D, "H+:::w1.234s")] public void ToString_PeriodTimespan(double? periodTimespan, string expected) { // Arrange FileRateLimitRule rule = new() { PeriodTimespan = periodTimespan, }; // Act var actual = rule.ToString(); // Assert Assert.Equal(expected, actual); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/FileRateLimitingTests.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.UnitTests.RateLimiting; public class FileRateLimitingTests { [Fact] public void FileRateLimitByAspNetRule_Ctor() { // Arrange, Act FileRateLimitByAspNetRule actual = new(); // Assert Assert.Null(actual.Policy); } [Fact] public void FileRateLimitByIpRule_Ctor() { // Arrange, Act FileRateLimitByIpRule actual = new(); // Assert Assert.Null(actual.IPWhitelist); } [Fact] public void FileRateLimitByMethodRule_Ctor() { // Arrange, Act FileRateLimitByMethodRule actual = new(); // Assert Assert.Null(actual.Methods); } [Fact] public void FileRateLimiting_Ctor() { // Arrange, Act FileRateLimiting actual = new(); // Assert Assert.Null(actual.ByHeader); Assert.Null(actual.ByMethod); Assert.Null(actual.ByIP); Assert.Null(actual.ByAspNet); Assert.Null(actual.Metadata); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/MemoryCacheRateLimitStorageTests.cs ================================================ using Microsoft.Extensions.Caching.Memory; using Ocelot.RateLimiting; namespace Ocelot.UnitTests.RateLimiting; public class MemoryCacheRateLimitStorageTests { private readonly Mock _cache; private readonly Mock _entry; private readonly MemoryCacheRateLimitStorage _storage; public MemoryCacheRateLimitStorageTests() { _cache = new Mock(); _entry = new Mock(); _storage = new MemoryCacheRateLimitStorage(_cache.Object); } [Fact] public void Set_ShouldCreateEntryAndSetValue() { // Arrange var id = "test-id"; var counter = new RateLimitCounter(DateTime.UtcNow, null, 1); TimeSpan expiration = TimeSpan.FromMinutes(5); _cache.Setup(c => c.CreateEntry(id)).Returns(_entry.Object); // Act _storage.Set(id, counter, expiration); // Assert _entry.VerifySet(e => e.Value = counter); _entry.VerifySet(e => e.AbsoluteExpirationRelativeToNow = expiration); _entry.Verify(e => e.Dispose()); _cache.Verify(c => c.CreateEntry(id), Times.Once); } [Fact] public void Exists_ShouldReturnTrue_WhenKeyExists() { // Arrange var id = "exists-id"; var counter = new RateLimitCounter(DateTime.UtcNow, null, 2); object boxed = counter; _cache.Setup(c => c.TryGetValue(id, out boxed)) .Returns(true); // Act var actual = _storage.Exists(id); // Assert Assert.True(actual); _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once); } [Fact] public void Exists_ShouldReturnFalse_WhenKeyDoesNotExist() { // Arrange var id = "missing-id"; object boxed = null; _cache.Setup(c => c.TryGetValue(id, out boxed)) .Returns(false); // Act var actual = _storage.Exists(id); // Assert Assert.False(actual); _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once); } [Fact] public void Get_ShouldReturnCounter_WhenKeyExists() { // Arrange var id = "get-id"; var counter = new RateLimitCounter(DateTime.UtcNow, null, 3); object boxed = counter; _cache.Setup(c => c.TryGetValue(id, out boxed)) .Returns(true); // Act var result = _storage.Get(id); // Assert Assert.NotNull(result); Assert.Equal(counter.Total, result.Value.Total); Assert.Equal(counter.StartedAt, result.Value.StartedAt); _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once); } [Fact] public void Get_ShouldReturnNull_WhenKeyDoesNotExist() { // Arrange var id = "null-id"; object boxed = null; _cache.Setup(c => c.TryGetValue(id, out boxed)) .Returns(false); // Act var actual = _storage.Get(id); // Assert Assert.Null(actual); _cache.Verify(c => c.TryGetValue(id, out boxed), Times.Once); } [Fact] public void Remove_ShouldCallCacheRemove() { // Arrange var id = "remove-id"; // Act _storage.Remove(id); // Assert _cache.Verify(c => c.Remove(id), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitCounterTests.cs ================================================ using Ocelot.RateLimiting; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitCounterTests { [Fact] public void Ctor() { // Arrange var startedAt = DateTime.Now; var exceededAt = startedAt + TimeSpan.FromSeconds(1); // Act var actual = new RateLimitCounter(startedAt, exceededAt, 3); // Assert Assert.Equal(startedAt, actual.StartedAt); Assert.Equal(exceededAt, actual.ExceededAt); Assert.Equal(3, actual.Total); } [Fact] [Trait("Feat", "1229")] // https://github.com/ThreeMammals/Ocelot/issues/1229 [Trait("PR", "2294")] // https://github.com/ThreeMammals/Ocelot/pull/2294 public void ToString_NoExceededAt() { // Arrange var today = new DateTime(2025, 9, 7); today = today.AddHours(12); today = today.AddMinutes(34); today = today.AddSeconds(56.789); today = today.AddMicroseconds(123.4567890); RateLimitCounter counter = new(today, default, 1); // Act var actual = counter.ToString(); // Assert Assert.Equal("1->(2025-09-07T12:34:56.7891234)", actual); } [Fact] [Trait("Feat", "1229")] // https://github.com/ThreeMammals/Ocelot/issues/1229 [Trait("PR", "2294")] // https://github.com/ThreeMammals/Ocelot/pull/2294 public void ToString_WithExceededAt() { // Arrange var today = new DateTime(2025, 9, 7); today = today.AddHours(1); today = today.AddMinutes(2); today = today.AddSeconds(3); today = today.AddMilliseconds(4); today = today.AddMicroseconds(5); TimeSpan shift = new(1, 2, 3, 4, 5, 6); DateTime? exceededAt = today + shift; RateLimitCounter counter = new(today, exceededAt, 2); // Act var actual = counter.ToString(); // Assert Assert.Equal("2->(2025-09-07T01:02:03.0040050)+1.02:03:04.0050060", actual); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitHeadersTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.RateLimiting; using System.Collections.Generic; using System.Reflection; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitHeadersTests { [Fact] public void Ctor_Created() { // Arrange HttpContext ctx = new DefaultHttpContext(); long limit = 3, remaining = 2; DateTime today = DateTime.Today; // Act RateLimitHeaders actual = new(ctx, limit, remaining, today); // Assert Assert.Equal(ctx, actual.Context); Assert.Equal(limit, actual.Limit); Assert.Equal(remaining, actual.Remaining); Assert.Equal(today, actual.Reset); } [Fact] public void Ctor_Parameterless() { // Arrange ConstructorInfo ctor = typeof(RateLimitHeaders).GetConstructor( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, binder: null, types: Type.EmptyTypes, modifiers: null); Assert.NotNull(ctor); Assert.False(ctor.IsPublic); // Act RateLimitHeaders actual = ctor.Invoke(null) as RateLimitHeaders; // Assert Assert.NotNull(actual); Assert.Equal(default, actual.Context); Assert.Equal(default, actual.Limit); Assert.Equal(default, actual.Remaining); Assert.Equal(default, actual.Reset); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsCreatorTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using System.Reflection; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitOptionsCreatorTests : UnitTest { private readonly RateLimitOptionsCreator _creator = new(); [Fact] [Trait("PR", "58")] // https://github.com/ThreeMammals/Ocelot/pull/58 [Trait("Release", "1.4.0")] public void Should_create_rate_limit_options() { // Arrange var route = new FileRoute { RateLimitOptions = new() { ClientWhitelist = new List(), Period = "Period", Limit = 1, Wait = "OneSecond", EnableRateLimiting = true, }, }; var globalConfig = new FileGlobalConfiguration { RateLimitOptions = new() { ClientIdHeader = "ClientIdHeader", EnableHeaders = true, QuotaExceededMessage = "QuotaMessage", RateLimitCounterPrefix = "RateLimitCounterPrefix", HttpStatusCode = 200, }, }; var options = route.RateLimitOptions; RateLimitOptions expected = new() { ClientIdHeader = "ClientIdHeader", ClientWhitelist = options.ClientWhitelist, EnableHeaders = true, EnableRateLimiting = true, StatusCode = 200, QuotaMessage = "QuotaMessage", KeyPrefix = "RateLimitCounterPrefix", Rule = new(options.Period, options.Wait, options.Limit.Value), }; bool enabled = true; // Act var result = _creator.Create(route, globalConfig); // Assert enabled.ShouldBeTrue(); result.ClientIdHeader.ShouldBe(expected.ClientIdHeader); result.ClientWhitelist.ShouldBe(expected.ClientWhitelist); result.EnableHeaders.ShouldBe(expected.EnableHeaders); result.EnableRateLimiting.ShouldBe(expected.EnableRateLimiting); result.StatusCode.ShouldBe(expected.StatusCode); result.QuotaMessage.ShouldBe(expected.QuotaMessage); result.KeyPrefix.ShouldBe(expected.KeyPrefix); result.Rule.Limit.ShouldBe(expected.Rule.Limit); result.Rule.Period.ShouldBe(expected.Rule.Period); result.Rule.Wait.ShouldBe(expected.Rule.Wait); } #region PR 2294 [Fact] [Trait("Feat", "1229")] // https://github.com/ThreeMammals/Ocelot/issues/1229 [Trait("PR", "2294")] // https://github.com/ThreeMammals/Ocelot/pull/2294 public void Create_ArgumentNullChecks() { // Arrange, Act, Assert Assert.Throws(() => _creator.Create(null, new())); Assert.Throws(() => _creator.Create(new FileRoute(), null)); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Create_GlobalRouteKeysCollectionIsNull_IsGlobalDefaultsToTrue() { // Arrange, Act, Assert: branch 1 FileRoute route = new(); FileGlobalConfiguration global = new(); var actual = _creator.Create(route, global); Assert.NotNull(actual); Assert.False(actual.EnableRateLimiting); // Arrange, Act, Assert: branch 2 global.RateLimitOptions = new(); // -> RouteKeys is null actual = _creator.Create(route, global); Assert.NotNull(actual); Assert.True(actual.EnableRateLimiting); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(false)] [InlineData(true)] public void Create_GlobalRouteKeys_ContainsRouteKey(bool contains) { // Arrange FileRoute route = new() { Key = contains ? "R1" : "?" }; FileGlobalConfiguration global = new() { RateLimitOptions = new() { RouteKeys = ["R1"], EnableRateLimiting = true, }, }; // Act var actual = _creator.Create(route, global); // Assert Assert.NotNull(actual); Assert.Equal(contains, actual.EnableRateLimiting); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(null, null, true)] [InlineData(false, null, false)] [InlineData(null, false, false)] [InlineData(false, false, false)] public void Create_DisabledRateLimiting(bool? ruleEnableRateLimiting, bool? globalEnableRateLimiting, bool expected) { // Arrange FileRoute route = new() { RateLimitOptions = new() { EnableRateLimiting = ruleEnableRateLimiting }, }; FileGlobalConfiguration global = new() { RateLimitOptions = new() { EnableRateLimiting = globalEnableRateLimiting }, }; // Act var actual = _creator.Create(route, global); // Assert Assert.NotNull(actual); Assert.Equal(expected, actual.EnableRateLimiting); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(true, null, false, "rule")] [InlineData(null, true, true, "globalRule")] [InlineData(true, true, false, "rule")] [InlineData(true, true, true, "rule")] [InlineData(null, null, true, "Oc-Client")] public void Create_ByHeaderRules(bool? hasRule, bool? hasGlobal, bool isGlobal, string expected) { // Arrange FileRoute route = new() { Key = "R1", RateLimitOptions = !hasRule.HasValue ? null : new() { ClientIdHeader = "rule" }, }; FileGlobalConfiguration global = new() { RateLimitOptions = !hasGlobal.HasValue ? null : new() { RouteKeys = [isGlobal ? "R1" : "?"], ClientIdHeader = "globalRule" }, }; // Act var actual = _creator.Create(route, global); // Assert Assert.NotNull(actual); Assert.Equal(expected, actual.ClientIdHeader); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void MergeHeaderRules_ArgumentNullChecks() { // Arrange MethodInfo method = _creator.GetType().GetMethod("MergeHeaderRules", BindingFlags.Instance | BindingFlags.NonPublic); FileRateLimitByHeaderRule rule = new(); FileGlobalRateLimitByHeaderRule globalRule = new(); // Act var ex1 = Assert.Throws(() => method?.Invoke(_creator, [null, globalRule])); var ex2 = Assert.Throws(() => method?.Invoke(_creator, [rule, null])); // Assert Assert.NotNull(ex1.InnerException); Assert.True(ex1.InnerException is ArgumentNullException); Assert.Equal(nameof(rule), ((ArgumentNullException)ex1.InnerException).ParamName); Assert.NotNull(ex2.InnerException); Assert.True(ex2.InnerException is ArgumentNullException); Assert.Equal(nameof(globalRule), ((ArgumentNullException)ex2.InnerException).ParamName); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void MergeHeaderRules_FromGlobal() { // Arrange FileRateLimitByHeaderRule rule = new(); FileGlobalRateLimitByHeaderRule global = new() { ClientIdHeader = "111", ClientWhitelist = ["222"], DisableRateLimitHeaders = null, EnableHeaders = false, EnableRateLimiting = true, HttpStatusCode = 300, StatusCode = 400, QuotaExceededMessage = "55", QuotaMessage = "66", RateLimitCounterPrefix = "77", KeyPrefix = "88", Period = "9s", PeriodTimespan = 10.0D, Wait = "11s", Limit = 12, }; MethodInfo method = _creator.GetType().GetMethod("MergeHeaderRules", BindingFlags.Instance | BindingFlags.NonPublic); // Act var actual = method?.Invoke(_creator, [rule, global]) as RateLimitOptions; // Assert Assert.NotNull(actual); Assert.Equal("111", actual.ClientIdHeader); Assert.Contains("222", actual.ClientWhitelist); Assert.False(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(300, actual.StatusCode); Assert.Equal("55", actual.QuotaMessage); Assert.Equal("77", actual.KeyPrefix); Assert.Equal("12/9s/w10s", actual.Rule.ToString()); } #endregion [Theory] [InlineData(true)] [InlineData(false)] public void Create_FileGlobalConfiguration(bool hasOptions) { // Arrange FileGlobalConfiguration global = new() { RateLimitOptions = !hasOptions ? null : new() { ClientIdHeader = "globalRule", }, }; // Act var actual = _creator.Create(global); // Assert Assert.NotNull(actual); Assert.Equal(hasOptions ? "globalRule" : "Oc-Client", actual.ClientIdHeader); Assert.Equal(hasOptions, actual.EnableRateLimiting); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitOptionsTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitOptionsTests { [Fact] public void Ctor_Parameterless() { // Arrange, Act RateLimitOptions actual = new(); // Assert Assert.Equal("Oc-Client", actual.ClientIdHeader); Assert.Empty(actual.ClientWhitelist); Assert.True(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(429, actual.StatusCode); Assert.Equal("API calls quota exceeded! Maximum admitted {0} per {1}.", actual.QuotaMessage); Assert.Equal("Ocelot.RateLimiting", actual.KeyPrefix); Assert.Equal(RateLimitRule.Empty, actual.Rule); } [Theory] [Trait("Feat", "1229")] [Trait("PR", "2294")] [InlineData(false)] [InlineData(true)] public void Ctor_Boolean(bool enableRateLimiting) { // Arrange, Act RateLimitOptions actual = new(enableRateLimiting); // Assert Assert.Equal(enableRateLimiting, actual.EnableRateLimiting); } [Theory] [InlineData(false)] [InlineData(true)] public void Ctor_Initialization(bool isEmpty) { // Arrange string[] clientIdHeader = new[] { nameof(RateLimitOptions.ClientIdHeader), RateLimitOptions.DefaultClientHeader }; List[] clientWhitelist = new[] { new List([nameof(RateLimitOptions.ClientWhitelist)]), default }; bool[] enableHeaders = new[] { true, default }; bool[] enableRateLimiting = new[] { true, default }; string[] rateLimitCounterPrefix = new[] { nameof(RateLimitOptions.KeyPrefix), RateLimitOptions.DefaultCounterPrefix }; string[] quotaExceededMessage = new[] { nameof(RateLimitOptions.QuotaMessage), RateLimitOptions.DefaultQuotaMessage }; RateLimitRule[] rateLimitRule = new[] { new RateLimitRule("1", "2", 3), default }; int[] httpStatusCode = new[] { 404, default }; // Act int i = isEmpty ? 1 : 0; RateLimitOptions actual = new( enableRateLimiting[i], clientIdHeader[i], clientWhitelist[i], enableHeaders[i], quotaExceededMessage[i], rateLimitCounterPrefix[i], rateLimitRule[i], httpStatusCode[i]); // Assert Assert.Equal(clientIdHeader[i], actual.ClientIdHeader); Assert.Equal(clientWhitelist[i] ?? [], actual.ClientWhitelist); Assert.Equal(enableHeaders[i], actual.EnableHeaders); Assert.Equal(enableRateLimiting[i], actual.EnableRateLimiting); Assert.Equal(rateLimitCounterPrefix[i], actual.KeyPrefix); Assert.Equal(quotaExceededMessage[i], actual.QuotaMessage); Assert.Equal(rateLimitCounterPrefix[i], actual.KeyPrefix); Assert.Equal(rateLimitRule[i], actual.Rule); Assert.Equal(httpStatusCode[i], actual.StatusCode); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFrom_ArgChecks() { // Arrange FileRateLimitByHeaderRule fromRule = null; // Act, Assert var ex = Assert.Throws(() => new RateLimitOptions(fromRule)); Assert.Equal(nameof(fromRule), ex.ParamName); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFrom_WithObsoleteProps() { // Arrange FileRateLimitByHeaderRule from = new() { ClientIdHeader = "1", ClientWhitelist = ["2"], DisableRateLimitHeaders = true, EnableRateLimiting = true, HttpStatusCode = 333, QuotaExceededMessage = "4", RateLimitCounterPrefix = "5", Period = "6", PeriodTimespan = 7D, Limit = 8, }; // Act RateLimitOptions actual = new(from); // Assert Assert.Equal("1", actual.ClientIdHeader); Assert.Contains("2", actual.ClientWhitelist); Assert.False(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(333, actual.StatusCode); Assert.Equal("4", actual.QuotaMessage); Assert.Equal("5", actual.KeyPrefix); Assert.Equal("8/6/w7s", actual.Rule.ToString()); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFrom_FileRateLimitByHeaderRule() { // Arrange FileRateLimitByHeaderRule from = new() { ClientIdHeader = "1", ClientWhitelist = ["2"], DisableRateLimitHeaders = null, EnableHeaders = true, EnableRateLimiting = true, HttpStatusCode = null, StatusCode = 444, QuotaExceededMessage = null, QuotaMessage = "55", RateLimitCounterPrefix = null, KeyPrefix = "66", Period = "7", PeriodTimespan = null, Wait = "8", Limit = 9, }; // Act RateLimitOptions actual = new(from); // Assert Assert.Equal("1", actual.ClientIdHeader); Assert.Contains("2", actual.ClientWhitelist); Assert.True(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(444, actual.StatusCode); Assert.Equal("55", actual.QuotaMessage); Assert.Equal("66", actual.KeyPrefix); Assert.Equal("9/7/w8", actual.Rule.ToString()); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFromFileRateLimitByHeaderRule_WithDefaults() { // Arrange FileRateLimitByHeaderRule from = new(); // Act RateLimitOptions actual = new(from); // Assert Assert.Equal("Oc-Client", actual.ClientIdHeader); Assert.Empty(actual.ClientWhitelist); Assert.True(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(429, actual.StatusCode); Assert.Equal("API calls quota exceeded! Maximum admitted {0} per {1}.", actual.QuotaMessage); Assert.Equal("Ocelot.RateLimiting", actual.KeyPrefix); Assert.Equal(RateLimitRule.Empty.ToString(), actual.Rule.ToString()); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFrom_RateLimitOptions() { // Arrange RateLimitOptions from = new() { ClientIdHeader = "1", ClientWhitelist = ["2"], EnableHeaders = true, EnableRateLimiting = true, StatusCode = 444, QuotaMessage = "55", KeyPrefix = "66", Rule = new("7", "8",9), }; // Act RateLimitOptions actual = new(from); // Assert Assert.Equal("1", actual.ClientIdHeader); Assert.Contains("2", actual.ClientWhitelist); Assert.True(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(444, actual.StatusCode); Assert.Equal("55", actual.QuotaMessage); Assert.Equal("66", actual.KeyPrefix); Assert.Equal("9/7/w8", actual.Rule.ToString()); } [Fact] [Trait("Feat", "1229")] [Trait("PR", "2294")] public void Ctor_CopyingFromRateLimitOptions_WithDefaults() { // Arrange RateLimitOptions from = new() { ClientIdHeader = string.Empty, ClientWhitelist = null, QuotaMessage = string.Empty, KeyPrefix = string.Empty, Rule = null, }; // Act RateLimitOptions actual = new(from); // Assert Assert.Equal("Oc-Client", actual.ClientIdHeader); Assert.Empty(actual.ClientWhitelist); Assert.True(actual.EnableHeaders); Assert.True(actual.EnableRateLimiting); Assert.Equal(429, actual.StatusCode); Assert.Equal("API calls quota exceeded! Maximum admitted {0} per {1}.", actual.QuotaMessage); Assert.Equal("Ocelot.RateLimiting", actual.KeyPrefix); Assert.Equal(RateLimitRule.Empty.ToString(), actual.Rule.ToString()); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitRuleTests.cs ================================================ using Ocelot.Configuration; namespace Ocelot.UnitTests.RateLimiting; [Trait("PR", "2294")] // https://github.com/ThreeMammals/Ocelot/pull/2294 public class RateLimitRuleTests { [Theory] [Trait("Feat", "585")] // https://github.com/ThreeMammals/Ocelot/issues/585 [Trait("Feat", "1915")] // https://github.com/ThreeMammals/Ocelot/issues/1915 [InlineData("1ms", 1D)] [InlineData("1s", 1_000D)] [InlineData("1m", 60_000D)] [InlineData("1h", 3_600_000D)] [InlineData("1d", 86_400_000D)] public void ParseTimespan_ShouldParseSupportedUnits(string oneUnit, double expected) { TimeSpan expectedSpan = TimeSpan.FromMilliseconds(expected); TimeSpan actual = RateLimitRule.ParseTimespan(oneUnit); Assert.Equal(expectedSpan, actual); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "1915")] [InlineData("1ms", 1.0D)] [InlineData("123ms", 123.0D)] [InlineData("2.ms", 2.0D)] [InlineData("3.0ms", 3.0D)] [InlineData(".4ms", 0.4D)] [InlineData("0.5ms", 0.5D)] [InlineData("0.678ms", 0.678D)] [InlineData("123.456ms", 123.456D)] public void ParseTimespan_ShouldParseMilliseconds(string value, double ms) { TimeSpan expected = TimeSpan.FromMilliseconds(ms); TimeSpan actual = RateLimitRule.ParseTimespan(value); Assert.Equal(expected, actual); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "1915")] [InlineData("1", 1.0D)] [InlineData("123", 123.0D)] [InlineData("2.", 2.0D)] [InlineData("3.0", 3.0D)] [InlineData(".4", 0.4D)] [InlineData("0.5", 0.5D)] [InlineData("0.678", 0.678D)] [InlineData("78.999", 78.999D)] public void ParseTimespan_ShouldParseWithoutUnitToMilliseconds(string value, double ms) { TimeSpan expected = TimeSpan.FromMilliseconds(ms); TimeSpan actual = RateLimitRule.ParseTimespan(value); Assert.Equal(expected, actual); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "1915")] [InlineData("-1ms", 1D)] [InlineData("-20s", 20_000D)] [InlineData("-3.0m", 180_000D)] public void ParseTimespan_ShouldParseNegativeAsPositive(string value, double ms) { TimeSpan expected = TimeSpan.FromMilliseconds(ms); TimeSpan actual = RateLimitRule.ParseTimespan(value); Assert.Equal(expected, actual); } [Theory] [Trait("Feat", "585")] [Trait("Feat", "1915")] [InlineData("1x", "The '1x' timespan cannot be converted to TimeSpan due to an unknown 'x' unit!")] [InlineData("x-bla-bla", $"The 'x-bla-bla' value doesn't include any digits, so it cannot be considered a number!")] public void ParseTimespan_ShouldThrowFormatException(string value, string message) { var error = Assert.Throws(() => RateLimitRule.ParseTimespan(value)); Assert.Equal(message, error.Message); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitingHeadersTests.cs ================================================ using Ocelot.RateLimiting; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitingHeadersTests { [Fact] public void Cctor_PropsInitialized() { // Arrange, Act, Assert Assert.Equal("Retry-After", RateLimitingHeaders.Retry_After); Assert.Equal("X-RateLimit-Limit", RateLimitingHeaders.X_RateLimit_Limit); Assert.Equal("X-RateLimit-Remaining", RateLimitingHeaders.X_RateLimit_Remaining); Assert.Equal("X-RateLimit-Reset", RateLimitingHeaders.X_RateLimit_Reset); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.RateLimiting; using Ocelot.Request.Middleware; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using _DownstreamRouteHolder_ = Ocelot.DownstreamRouteFinder.DownstreamRouteHolder; using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitingMiddlewareTests : UnitTest { private readonly IRateLimitStorage _storage; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly Mock _contextAccessor; private readonly RateLimitingMiddleware _middleware; private readonly RequestDelegate _next; private readonly IRateLimiting _rateLimiting; private readonly List _downstreamResponses; private readonly string _url; private Func _loggerMessage; public RateLimitingMiddlewareTests() { _url = "http://localhost:" + PortFinder.GetRandomPort(); var cacheEntryOptions = new MemoryCacheOptions(); _storage = new MemoryCacheRateLimitStorage(new MemoryCache(cacheEntryOptions)); _loggerFactory = new Mock(); _logger = new Mock(); _logger.Setup(x => x.LogInformation(It.IsAny>())) .Callback>(f => _loggerMessage = f); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _rateLimiting = new _RateLimiting_(_storage); _contextAccessor = new Mock(); _middleware = new RateLimitingMiddleware(_next, _loggerFactory.Object, _rateLimiting, _contextAccessor.Object); _downstreamResponses = new(); } [Fact] [Trait("Feat", "37")] public async Task Should_call_middleware_and_ratelimiting() { // Arrange const long limit = 3L; var downstreamRoute = GivenDownstreamRoute(rule: new("1s", "100s", limit)); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); // Act, Assert await WhenICallTheMiddlewareMultipleTimes(limit, dsHolder); _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); // Act, Assert: the next request should fail await WhenICallTheMiddlewareMultipleTimes(3, dsHolder); _downstreamResponses.ShouldNotBeNull(); for (int i = 0; i < _downstreamResponses.Count; i++) { var response = _downstreamResponses[i].ShouldNotBeNull(); response.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no is {i}"); var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); body.ShouldBe("Exceeding!"); } } [Theory] [Trait("Feat", "37")] [InlineData(false)] [InlineData(true)] public async Task Should_not_call_middleware_with_disabled_ratelimiting(bool hasOptions) { // Arrange RateLimitOptions options = hasOptions ? new(false) : null; var downstreamRoute = new DownstreamRouteBuilder() .WithRateLimitOptions(options) .Build(); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); // Act var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder); // Assert _downstreamResponses.ShouldNotBeNull(); _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); ShouldLogInformation("Rate limiting is disabled for route '?' via the EnableRateLimiting option."); } [Fact] [Trait("Feat", "37")] public async Task Should_call_middleware_with_whitelisted_client() { // Arrange var opts = GivenRateLimitOptions(new("1s", "100s", 3)); var options = new RateLimitOptions(opts) { ClientWhitelist = ["ocelotclient2"], }; var downstreamRoute = GivenDownstreamRoute(options); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); // Act await WhenICallTheMiddlewareWithWhiteClient(dsHolder); // Assert _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); } [Fact] [Trait("Bug", "1305 ")] // https://github.com/ThreeMammals/Ocelot/issues/1305 [Trait("PR", "1307 ")] // https://github.com/ThreeMammals/Ocelot/pull/1307 public async Task ShouldPopulateRateLimitingHeaders() { // Arrange RateLimitOptions options = new() { EnableHeaders = true, ClientIdHeader = "ClientId", Rule = new("1s", "1s", 3), }; var downstreamRoute = GivenDownstreamRoute(options); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); var originalContext = new DefaultHttpContext(); _contextAccessor.SetupGet(x => x.HttpContext).Returns(originalContext); // Act var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, null, originalContext); // Assert originalContext.Response.ShouldNotBeNull(); _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Once); var msg = _loggerMessage.ShouldNotBeNull().Invoke(); msg.ShouldStartWith("Route '?' must return rate limiting headers with the following data: 2/3 resets at "); // Route '?' must return rate limiting headers with the following data: 2/3 resets at 2025-09-11T13:37:13.7973731Z } [Theory] [Trait("Bug", "1305 ")] [Trait("PR", "1307 ")] [InlineData(false, false, 0)] [InlineData(false, true, 0)] [InlineData(true, false, 0)] [InlineData(true, true, 1)] public async Task ShouldPopulateRateLimitingHeaders_Branches(bool enableHeaders, bool hasContext, int loggedTimes) { // Arrange RateLimitOptions options = new() { EnableHeaders = enableHeaders, ClientIdHeader = "ClientId", Rule = new("1s", "1s", 3), }; var downstreamRoute = GivenDownstreamRoute(options); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); var originalContext = hasContext ? new DefaultHttpContext() : null; _contextAccessor.SetupGet(x => x.HttpContext).Returns(originalContext); // Act var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, null, originalContext); // Assert _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Exactly(loggedTimes)); } [Fact] [Trait("Bug", "1305 ")] [Trait("PR", "1307 ")] public async Task SetRateLimitHeaders() { // Arrange var today = new DateTime(2025, 9, 11, 3, 4, 5, 6, 7, DateTimeKind.Utc); var context = new DefaultHttpContext(); var state = new RateLimitHeaders(context, 3, 2, today); var method = _middleware.GetType().GetMethod("SetRateLimitHeaders", BindingFlags.Instance | BindingFlags.NonPublic); // Act Task t = method.Invoke(_middleware, [state]) as Task; await t; // Assert Assert.True(t.IsCompleted); var headers = context.Response.Headers; Assert.NotEmpty(headers); Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Limit)); Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Remaining)); Assert.True(headers.ContainsKey(RateLimitingHeaders.X_RateLimit_Reset)); Assert.Equal("3", headers[RateLimitingHeaders.X_RateLimit_Limit]); Assert.Equal("2", headers[RateLimitingHeaders.X_RateLimit_Remaining]); Assert.Equal("2025-09-11T03:04:05.0060070Z", headers[RateLimitingHeaders.X_RateLimit_Reset]); } [Fact] [Trait("Bug", "1590")] public async Task Invoke_PeriodTimespanValueIsGreaterThanPeriod_StatusNotEqualTo429() { // Arrange const long limit = 100L; var rule = new RateLimitRule("1s", "30s", limit); // bug scenario var downstreamRoute = GivenDownstreamRoute(rule: rule); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); // Act, Assert: 100 requests must be successful var contexts = await WhenICallTheMiddlewareMultipleTimes(limit, dsHolder); // make 100 requests, but not exceed the limit _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull()); contexts.ForEach(ctx => { ctx.ShouldNotBeNull(); ctx.Items.Errors().ShouldNotBeNull().ShouldBeEmpty(); // no errors ctx.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); // not 429 aka TooManyRequests }); // Act, Assert: the next 101st request should fail contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder); _downstreamResponses.ShouldNotBeNull(); var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull(); ds.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests, $"Downstream Response no {limit + 1}"); var body = await ds.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); body.ShouldBe("Exceeding!"); contexts[0].Items.Errors().ShouldNotBeNull().ShouldNotBeEmpty(); // having errors contexts[0].Items.Errors().Single().HttpStatusCode.ShouldBe((int)HttpStatusCode.TooManyRequests); } [Fact] [Trait("Feat", "37")] [Trait("Feat", "585")] [Trait("PR", "2294")] public async Task Invoke_NoClientHeader_Status503_ShouldLogWarning() { // Arrange var downstreamRoute = GivenDownstreamRoute(); var route = GivenRoute(downstreamRoute); var dsHolder = new _DownstreamRouteHolder_(new(), route); // Act var contexts = await WhenICallTheMiddlewareMultipleTimes(1, dsHolder, "bla-bla-header:spy"); // Assert var ctx = contexts[0].ShouldNotBeNull(); var errors = ctx.Items.Errors().ShouldNotBeNull(); var err = Assert.Single(errors); Assert.IsType(err); Assert.Equal("Rate limiting client could not be identified for the route '?' due to a missing or unknown client ID header required by rule '3/1s/w1s'!", err.Message); var ds = _downstreamResponses.SingleOrDefault().ShouldNotBeNull(); Assert.Equal(HttpStatusCode.ServiceUnavailable, ds.StatusCode); var body = await ds.Content.ReadAsStringAsync(TestContext.Current.CancellationToken); Assert.Equal("Rate limiting client could not be identified for the route '?' due to a missing or unknown client ID header required by rule '3/1s/w1s'!", body); _logger.Verify(x => x.LogWarning(err.Message), Times.Once); } private static RateLimitOptions GivenRateLimitOptions(RateLimitRule rule = null, [CallerMemberName] string testName = null) => new( enableRateLimiting: true, clientIdHeader: "ClientId", clientWhitelist: [], enableHeaders: true, quotaExceededMessage: "Exceeding!", rateLimitCounterPrefix: testName, rule ?? new("1s", "1s", 3), StatusCodes.Status429TooManyRequests); private static DownstreamRoute GivenDownstreamRoute(RateLimitOptions options = null, RateLimitRule rule = null, [CallerMemberName] string testName = null) => new DownstreamRouteBuilder() .WithRateLimitOptions(options ?? GivenRateLimitOptions(rule)) .WithUpstreamHttpMethod([HttpMethods.Get]) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().Build()) .WithLoadBalancerKey(testName) .Build(); private static Route GivenRoute(DownstreamRoute dr) => new() { DownstreamRoute = [dr], UpstreamHttpMethod = [HttpMethod.Get], }; private async Task> WhenICallTheMiddlewareMultipleTimes(long times, _DownstreamRouteHolder_ holder, string header = null, HttpContext originalContext = null) { var contexts = new List(); _downstreamResponses.Clear(); for (var i = 0; i < times; i++) { var context = originalContext ?? new DefaultHttpContext(); var stream = GetFakeStream($"{i}"); context.Response.Body = stream; context.Response.RegisterForDispose(stream); context.Items.UpsertDownstreamRoute(holder.Route.DownstreamRoute[0]); context.Items.UpsertTemplatePlaceholderNameAndValues(holder.TemplatePlaceholderNameAndValues); context.Items.UpsertDownstreamRoute(holder); var request = new HttpRequestMessage(new HttpMethod("GET"), _url); context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); header ??= "ClientId:ocelotclient1"; var hdr = header.Split(':'); context.Request.Headers.TryAdd(hdr[0], hdr[1]); contexts.Add(context); await _middleware.Invoke(context); _downstreamResponses.Add(context.Items.DownstreamResponse()); } return contexts; } private static MemoryStream GetFakeStream(string str) { byte[] data = Encoding.ASCII.GetBytes(str); return new MemoryStream(data, 0, data.Length); } private async Task WhenICallTheMiddlewareWithWhiteClient(_DownstreamRouteHolder_ holder) { const string ClientId = "ocelotclient2"; for (var i = 0; i < 10; i++) { var context = new DefaultHttpContext(); var stream = GetFakeStream($"{i}"); context.Response.Body = stream; context.Response.RegisterForDispose(stream); context.Items.UpsertDownstreamRoute(holder.Route.DownstreamRoute[0]); context.Items.UpsertTemplatePlaceholderNameAndValues(holder.TemplatePlaceholderNameAndValues); context.Items.UpsertDownstreamRoute(holder); var request = new HttpRequestMessage(new HttpMethod("GET"), _url); request.Headers.Add("ClientId", ClientId); context.Items.UpsertDownstreamRequest(new DownstreamRequest(request)); context.Request.Headers.TryAdd("ClientId", ClientId); await _middleware.Invoke(context); _downstreamResponses.Add(context.Items.DownstreamResponse()); } } private void ShouldLogInformation(string expected) { _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Once); var msg = _loggerMessage.ShouldNotBeNull().Invoke(); msg.ShouldBe(expected); } } ================================================ FILE: test/Ocelot.UnitTests/RateLimiting/RateLimitingTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.RateLimiting; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using _RateLimiting_ = Ocelot.RateLimiting.RateLimiting; namespace Ocelot.UnitTests.RateLimiting; public class RateLimitingTests : RateLimitingTestsBase { [Theory] [Trait("Feat", "37")] [InlineData(null)] [InlineData("")] public void ToTimespan_EmptyValue_ShouldReturnZero(string empty) { // Arrange, Act var actual = _sut.ToTimespan(empty); // Assert Assert.Equal(TimeSpan.Zero, actual); } [Theory] [Trait("Feat", "37")] [InlineData("1a")] [InlineData("2unknown")] public void ToTimespan_UnknownType_ShouldThrowFormatException(string timespan) { // Arrange, Act, Assert Assert.Throws( () => _sut.ToTimespan(timespan)); } [Theory] [Trait("Feat", "37")] [InlineData("1s", 1 * TimeSpan.TicksPerSecond)] [InlineData("2m", 2 * TimeSpan.TicksPerMinute)] [InlineData("3h", 3 * TimeSpan.TicksPerHour)] [InlineData("4d", 4 * TimeSpan.TicksPerDay)] public void ToTimespan_KnownType_HappyPath(string timespan, long ticks) { // Arrange var expected = TimeSpan.FromTicks(ticks); // Act var actual = _sut.ToTimespan(timespan); // Assert Assert.Equal(expected, actual); } [Fact] [Trait("PR", "1592")] public void Count_NoEntry_StartCounting() { // Arrange DateTime now = DateTime.UtcNow; RateLimitCounter? arg1 = null; // No Entry RateLimitRule arg2 = null; // Act RateLimitCounter actual = _sut.Count(arg1, arg2, now); // Assert Assert.Equal(1L, actual.Total); Assert.True(now - actual.StartedAt < TimeSpan.FromSeconds(1.0D)); } [Fact] [Trait("PR", "1592")] public void Count_EntryHasNotExpired_IncrementedRequestCount() { // Arrange long total = 2; DateTime now = DateTime.UtcNow; RateLimitCounter? arg1 = new RateLimitCounter(now, null, total); // entry has not expired RateLimitRule arg2 = new("1s", "1s", total + 1); // with not exceeding limit // Act RateLimitCounter actual = _sut.Count(arg1, arg2, now); // Assert Assert.Equal(total + 1, actual.Total); // incremented request count Assert.Equal(arg1.Value.StartedAt, actual.StartedAt); // starting point has not changed } [Fact] [Trait("PR", "1592")] public void Count_EntryHasNotExpiredAndExceedingLimit_IncrementedRequestCountWithRenewedStartMoment() { // Arrange long total = 2; DateTime now = DateTime.UtcNow; RateLimitCounter? arg1 = new RateLimitCounter(now, null, total); // entry has not expired RateLimitRule arg2 = new("1s", "1s", 1L); // Act RateLimitCounter actual = _sut.Count(arg1, arg2, now); // Assert Assert.Equal(total + 1, actual.Total); // incremented request count Assert.InRange(actual.StartedAt, arg1.Value.StartedAt, now); // starting point has renewed and it is between StartedAt and Now } [Fact] [Trait("PR", "1592")] public void Count_RateLimitExceeded_StartedCounting() { // Arrange long total = 3, limit = total - 1; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", "2s", limit); // rate limit exceeded var entry = new RateLimitCounter( now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds), now.AddSeconds(-rule.WaitSpan.TotalSeconds), total); // Entry has expired // Act var futureIsNow = now.AddMilliseconds(1); // let's move to the future to allow the waiting period to pass RateLimitCounter actual = _sut.Count(entry, rule, futureIsNow); // Assert Assert.Equal(1L, actual.Total); // started counting, the counter was changed Assert.Equal(futureIsNow, actual.StartedAt); // started now } [Fact] [Trait("PR", "1592")] public void Count_PeriodIsElapsedAndWaitPeriodIsElapsed_StartedNewCountingPeriod() { // Arrange long total = 3, limit = 3; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", "1s", limit); RateLimitCounter? entry = new( now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds), // 2 seconds ago now.AddSeconds(-rule.WaitSpan.TotalSeconds), // 1 second ago total); // Entry is about to expire // Act, Assert 1 RateLimitCounter actual = _sut.Count(entry, rule, now); // at the moment of wait period elapsing, inclusively Assert.Equal(4, actual.Total); // started counting Assert.True(actual.ExceededAt.HasValue); // old counter is valid // Act, Assert 2 var futureIsNow = now.AddMilliseconds(1); // let's move to the future to allow the waiting period to pass actual = _sut.Count(entry, rule, futureIsNow); Assert.Equal(1L, actual.Total); // started counting Assert.Equal(futureIsNow, actual.StartedAt); // started now } [Fact] [Trait("PR", "1592")] public void ProcessRequest_QuotaExceededAndWaitPeriodElapsed_StartedCountingViaResettingCounter() { // Arrange const string fixedWindow = "3s", waitWindow = "2s"; RateLimitRule rule = new(fixedWindow, waitWindow, 2); DateTime now = DateTime.UtcNow, startedAt = now.AddSeconds(-rule.PeriodSpan.TotalSeconds); DateTime? exceededAt = null; long totalRequests = 2L; TimeSpan expiration = TimeSpan.Zero; var (identity, options) = SetupProcessRequest(fixedWindow, waitWindow, totalRequests, () => new RateLimitCounter(startedAt, exceededAt, totalRequests), (value) => expiration = value); // Act 1 var counter = _sut.ProcessRequest(identity, options, now); // Assert 1 Assert.Equal(3L, counter.Total); // old counting -> 3 Assert.Equal(startedAt, counter.StartedAt); // starting point was not changed Assert.True(counter.ExceededAt.HasValue); // exceeded Assert.Equal(now, counter.ExceededAt.Value); // exceeded now, in the same second // Arrange 2 startedAt = counter.StartedAt; // move to past exceededAt = counter.ExceededAt; // move to past totalRequests = counter.Total; // 3 now += rule.WaitSpan; // don't wait, just move to future // Act 2 var actual = _sut.ProcessRequest(identity, options, now); // Assert Assert.Equal(1L, actual.Total); // started counting Assert.Equal(actual.StartedAt, now); // starting point has renewed and it is Now Assert.Null(actual.ExceededAt); _storage.Verify(x => x.Remove(It.IsAny()), Times.Never()); // Once()? Seems Remove is never called because of renewing _storage.Verify(x => x.Get(It.IsAny()), Times.Exactly(2)); _storage.Verify(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny()), Times.Exactly(2)); Assert.Equal(6, expiration.TotalSeconds); } [Fact] [Trait("PR", "2294")] public void RetryAfter_NoQuotaExceeding_NoNeedToRetry() { // Arrange long total = 2, limit = 3; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", "1s", limit); RateLimitCounter counter = new( now.AddSeconds(-rule.PeriodSpan.TotalSeconds / 2), null, total); // Act double actual = _sut.RetryAfter(counter, rule, now); // Assert Assert.Equal(0.0, actual); } [Theory] [Trait("PR", "2294")] [InlineData(null)] [InlineData("")] [InlineData(RateLimitRule.ZeroWait)] public void RetryAfter_DoNotWait_RetryAfterTheHalfOfPeriod(string doNotWait) { // Arrange long total = 4, limit = 3; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", doNotWait, limit); RateLimitCounter counter = new( startedAt: now.AddSeconds(-rule.PeriodSpan.TotalSeconds / 2), exceededAt: now, totalRequests: total); // Act double actual = _sut.RetryAfter(counter, rule, now); // Assert Assert.Equal(0.5, actual); } [Fact] [Trait("PR", "2294")] public void RetryAfter_ExceedingInWaitingWindow_RetryAfterTheQuarterOfWaitPeriod() { // Arrange long total = 4, limit = 3; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", "1s", limit); RateLimitCounter counter = new( startedAt: now.AddSeconds(-(rule.PeriodSpan.TotalSeconds / 2) - (rule.WaitSpan.TotalSeconds / 4 * 3)), exceededAt: now.AddSeconds(-(rule.WaitSpan.TotalSeconds / 4 * 3)), totalRequests: total); // Act double actual = _sut.RetryAfter(counter, rule, now); // Assert Assert.Equal(0.25, actual); } [Fact] [Trait("PR", "2294")] public void RetryAfter_Exceeding_WaitingPeriodElapsed_NoNeedToRetry() { // Arrange long total = 4, limit = 3; DateTime now = DateTime.UtcNow; RateLimitRule rule = new("1s", "1s", limit); RateLimitCounter counter = new( startedAt: now.AddSeconds(-rule.PeriodSpan.TotalSeconds - rule.WaitSpan.TotalSeconds), exceededAt: now.AddSeconds(-rule.WaitSpan.TotalSeconds), totalRequests: total); // Act double actual = _sut.RetryAfter(counter, rule, now); // Assert Assert.Equal(-1.0, actual); } [Collection(nameof(SequentialTests))] public class Sequential : RateLimitingTestsBase { [Fact] [Trait("Bug", "1590")] public void ProcessRequest_PeriodTimespanValueIsGreaterThanPeriod_ExpectedBehaviorAndExpirationInPeriod() { // The test is stable in Linux and Windows only Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.OSX), "Skip in MacOS because the test is very unstable"); // Arrange: user scenario const long limit = 100L, requestsPerSecond = 20L; const string fixedWindow = "1s", waitWindow = "30s"; RateLimitRule rule = new(fixedWindow, waitWindow, 2); // Arrange: setup DateTime now = DateTime.UtcNow; DateTime? startedAt = null; TimeSpan expiration = TimeSpan.Zero; long total = 1L, count = requestsPerSecond; RateLimitCounter? current = null; var (identity, options) = SetupProcessRequest(fixedWindow, waitWindow, limit, () => current, (value) => expiration = value); // Arrange 20 requests per period (1 sec) var periodMilliseconds = rule.PeriodSpan.TotalMilliseconds; int delay = (int)((periodMilliseconds - 200) / requestsPerSecond); // 20 requests per 1 second while (count > 0L) { // Act var actual = _sut.ProcessRequest(identity, options, now); // life hack for the 1st request if (count == requestsPerSecond) { startedAt = actual.StartedAt; // for the 1st request get expected value } // Assert Assert.True(actual.Total < limit); actual.Total.ShouldBe(total++, $"Count is {count}"); Assert.Equal(startedAt, actual.StartedAt); // starting point is not changed Assert.Null(actual.ExceededAt); // no exceeding at all Assert.Equal(32, expiration.TotalSeconds); // Arrange: next micro test current = actual; Thread.Sleep(delay); count--; } Assert.NotEqual(rule.WaitSpan, expiration); // Not Wait period expiration Assert.Equal(32, expiration.TotalSeconds); // last 20th request was in counting period } } } public class RateLimitingTestsBase { protected readonly Mock _storage; protected readonly _RateLimiting_ _sut; public RateLimitingTestsBase() { _storage = new(); _sut = new(_storage.Object); } protected (ClientRequestIdentity Identity, RateLimitOptions Options) SetupProcessRequest(string fixedWindow, string waitWindow, long limit, Func counterFactory, Action expirationAction, [CallerMemberName] string testName = "") { ClientRequestIdentity identity = new(nameof(RateLimitingTests) + "/" + testName, HttpMethods.Get); RateLimitOptions options = new() { EnableRateLimiting = true, KeyPrefix = nameof(_RateLimiting_.ProcessRequest), Rule = new(fixedWindow, waitWindow, limit), }; _storage.Setup(x => x.Get(It.IsAny())) .Returns(counterFactory); // counter value factory _storage.Setup(x => x.Remove(It.IsAny())) .Verifiable(); expirationAction?.Invoke(TimeSpan.Zero); _storage.Setup(x => x.Set(It.IsAny(), It.IsAny(), It.IsAny())) .Callback((id, counter, expirationTime) => expirationAction?.Invoke(expirationTime)) .Verifiable(); return (identity, options); } } ================================================ FILE: test/Ocelot.UnitTests/Repository/HttpDataRepositoryTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Infrastructure.RequestData; namespace Ocelot.UnitTests.Repository; public class HttpDataRepositoryTests : UnitTest { private readonly HttpDataRepository _repository; private readonly HttpContextAccessor _contextAccesor; public HttpDataRepositoryTests() { _contextAccesor = new() { HttpContext = new DefaultHttpContext(), }; _repository = new HttpDataRepository(_contextAccesor); } [Fact] public void Should_add_item() { // Arrange const string key = "blahh"; var toAdd = new[] { 1, 2, 3, 4 }; // Act _repository.Add(key, toAdd); // Assert _contextAccesor.HttpContext.Items.TryGetValue(key, out var obj).ShouldBeTrue(); obj.ShouldNotBeNull(); var arr = (int[])obj; arr.ShouldNotBeNull(); arr.ShouldContain(4); } [Fact] public void Should_get_item() { // Arrange const string key = "chest"; var data = new[] { 5435345 }; _contextAccesor.HttpContext.Items.Add(key, data); // Act var result = _repository.Get(key); // Assert result.IsError.ShouldBeFalse(); result.Data.ShouldNotBeNull(); } } ================================================ FILE: test/Ocelot.UnitTests/Request/Creator/DownstreamRequestCreatorTests.cs ================================================ using Ocelot.Infrastructure; using Ocelot.Request.Creator; namespace Ocelot.UnitTests.Request.Creator; public class DownstreamRequestCreatorTests : UnitTest { private readonly Mock _framework; private readonly DownstreamRequestCreator _downstreamRequestCreator; public DownstreamRequestCreatorTests() { _framework = new Mock(); _downstreamRequestCreator = new DownstreamRequestCreator(_framework.Object); } [Fact] public async Task Should_create_downstream_request() { // Arrange var request = new HttpRequestMessage(HttpMethod.Get, "http://www.test.com"); var content = new StringContent("test"); request.Content = content; _framework.Setup(x => x.Get()).Returns(string.Empty); // Act var result = _downstreamRequestCreator.Create(request); // Assert: Then The Downstream Request Has A Body result.ShouldNotBeNull(); result.Method.ToLower().ShouldBe("get"); result.Scheme.ToLower().ShouldBe("http"); result.Host.ToLower().ShouldBe("www.test.com"); var resultContent = await result.ToHttpRequestMessage().Content.ReadAsStringAsync(TestContext.Current.CancellationToken); resultContent.ShouldBe("test"); } [Fact] public void Should_remove_body_for_http_methods() { // Arrange var methods = new List { HttpMethod.Get, HttpMethod.Head, HttpMethod.Delete, HttpMethod.Trace }; var request = new HttpRequestMessage(HttpMethod.Get, "http://www.test.com"); var content = new StringContent("test"); request.Content = content; methods.ForEach(m => { _framework.Setup(x => x.Get()).Returns(".NET Framework"); // Act var result = _downstreamRequestCreator.Create(request); // Assert: Then The Downstream Request Does Not Have A Body result.ShouldNotBeNull(); result.Method.ToLower().ShouldBe("get"); result.Scheme.ToLower().ShouldBe("http"); result.Host.ToLower().ShouldBe("www.test.com"); result.ToHttpRequestMessage().Content.ShouldBeNull(); }); } } ================================================ FILE: test/Ocelot.UnitTests/Request/DownstreamRequestInitialiserMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Infrastructure; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Creator; using Ocelot.Request.Mapper; using Ocelot.Request.Middleware; namespace Ocelot.UnitTests.Request; public class DownstreamRequestInitialiserMiddlewareTests : UnitTest { private readonly DownstreamRequestInitialiserMiddleware _middleware; private readonly DefaultHttpContext _httpContext; private readonly Mock _next; private readonly Mock _requestMapper; private HttpRequestMessage _mappedRequest; private readonly Exception _testException; public DownstreamRequestInitialiserMiddlewareTests() { _httpContext = new DefaultHttpContext(); _requestMapper = new Mock(); _next = new Mock(); var logger = new Mock(); _testException = new Exception("test exception"); var loggerFactory = new Mock(); loggerFactory.Setup(lf => lf.CreateLogger()) .Returns(logger.Object); _middleware = new DownstreamRequestInitialiserMiddleware( _next.Object, loggerFactory.Object, _requestMapper.Object, new DownstreamRequestCreator(new FrameworkDescription())); _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build()); } [Fact] public async Task Should_handle_valid_httpRequest() { // Arrange GivenTheMapperWillReturnAMappedRequest(); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheContexRequestIsMappedToADownstreamRequest(); ThenTheDownstreamRequestIsStored(); ThenTheNextMiddlewareIsInvoked(); ThenTheDownstreamRequestMethodIs("GET"); } [Fact] public async Task Should_map_downstream_route_method_to_downstream_request() { // Arrange GivenTheMapperWillReturnAMappedRequest(); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheContexRequestIsMappedToADownstreamRequest(); ThenTheDownstreamRequestIsStored(); ThenTheNextMiddlewareIsInvoked(); ThenTheDownstreamRequestMethodIs("GET"); } [Fact] public async Task Should_handle_mapping_failure() { // Arrange _requestMapper.Setup(rm => rm.Map(It.IsAny(), It.IsAny())) .Throws(_testException); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.DownstreamRequest().ShouldBeNull(); _httpContext.Items.Errors().Count.ShouldBe(1); _httpContext.Items.Errors().First().ShouldBeOfType(); _httpContext.Items.Errors().First().Message.ShouldBe($"Error when parsing incoming request, exception: {_testException}"); _next.Verify(n => n(It.IsAny()), Times.Never); } private void ThenTheDownstreamRequestMethodIs(string expected) { _httpContext.Items.DownstreamRequest().Method.ShouldBe(expected); } private void GivenTheMapperWillReturnAMappedRequest() { _mappedRequest = new HttpRequestMessage(HttpMethod.Get, "http://www.bbc.co.uk"); _requestMapper .Setup(rm => rm.Map(It.IsAny(), It.IsAny())) .Returns(_mappedRequest); } private void ThenTheContexRequestIsMappedToADownstreamRequest() { _requestMapper.Verify(rm => rm.Map(_httpContext.Request, _httpContext.Items.DownstreamRoute()), Times.Once); } private void ThenTheDownstreamRequestIsStored() { _httpContext.Items.DownstreamRequest().ShouldNotBeNull(); } private void ThenTheNextMiddlewareIsInvoked() { _next.Verify(n => n(_httpContext), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Request/DownstreamRequestTests.cs ================================================ using Ocelot.Request.Middleware; namespace Ocelot.UnitTests.Request; public class DownstreamRequestTests { [Fact] public void Should_have_question_mark_with_question_mark_prefixed() { // Arrange var requestMessage = new HttpRequestMessage { RequestUri = new Uri("https://example.com/a?b=c"), }; var downstreamRequest = new DownstreamRequest(requestMessage); // Act var result = downstreamRequest.ToHttpRequestMessage(); // Assert result.RequestUri.Query.ShouldBe("?b=c"); } } ================================================ FILE: test/Ocelot.UnitTests/Request/Mapper/RequestMapperTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Request.Mapper; using System.Security.Cryptography; using System.Text; using Microsoft.Net.Http.Headers; namespace Ocelot.UnitTests.Request.Mapper; public class RequestMapperTests : UnitTest { private readonly HttpRequest _inputRequest; private readonly RequestMapper _requestMapper; private HttpRequestMessage _mappedRequest; private List> _inputHeaders; private DownstreamRoute _downstreamRoute; public RequestMapperTests() { var httpContext = new DefaultHttpContext(); _inputRequest = httpContext.Request; _requestMapper = new RequestMapper(); } [Theory] [InlineData("https", "my.url:123", "/abc/DEF", "?a=1&b=2", "https://my.url:123/abc/DEF?a=1&b=2")] [InlineData("http", "blah.com", "/d ef", "?abc=123", "http://blah.com/d%20ef?abc=123")] // note! the input is encoded when building the input request [InlineData("http", "myusername:mypassword@abc.co.uk", null, null, "http://myusername:mypassword@abc.co.uk/")] [InlineData("http", "點看.com", null, null, "http://xn--c1yn36f.com/")] [InlineData("http", "xn--c1yn36f.com", null, null, "http://xn--c1yn36f.com/")] public void Should_map_valid_request_uri(string scheme, string host, string path, string queryString, string expectedUri) { // Arrange _inputRequest.Method = "GET"; _inputRequest.Scheme = scheme; GivenTheInputRequestHasHost(host); GivenTheInputRequestHasPath(path); GivenTheInputRequestHasQueryString(queryString); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert Assert.NotNull(_mappedRequest.RequestUri); _mappedRequest.RequestUri.OriginalString.ShouldBe(expectedUri); } [Theory] [InlineData("ftp", "google.com", "/abc/DEF", "?a=1&b=2")] public void Should_error_on_unsupported_request_uri(string scheme, string host, string path, string queryString) { // Arrange _inputRequest.Method = "GET"; _inputRequest.Scheme = scheme; GivenTheInputRequestHasHost(host); GivenTheInputRequestHasPath(path); GivenTheInputRequestHasQueryString(queryString); // Act, Assert Assert.Throws(() => _requestMapper.Map(_inputRequest, _downstreamRoute)); } [Theory] [InlineData("GET")] [InlineData("POST")] [InlineData("WHATEVER")] public void Should_map_method(string method) { // Arrange _inputRequest.Method = method; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert _mappedRequest.Method.ToString().ShouldBe(method); } [Theory] [InlineData("", "GET")] [InlineData(null, "GET")] [InlineData("POST", "POST")] public void Should_use_downstream_route_method_if_set(string input, string expected) { // Arrange _inputRequest.Method = "GET"; _downstreamRoute = new DownstreamRouteBuilder() .WithDownStreamHttpMethod(input) .WithDownstreamHttpVersion(new Version("1.1")) .Build(); GivenTheInputRequestHasAValidUri(); // Act WhenMapped(); // Assert _mappedRequest.Method.ToString().ShouldBe(expected); } [Fact] public void Should_map_all_headers() { // Arrange: Given The Input Request Has Headers var abcVals = new[] { "123", "456" }; var defVals = new[] { "789", "012" }; _inputHeaders = new() { new("abc", new StringValues(abcVals)), new("def", new StringValues(defVals)), }; foreach (var inputHeader in _inputHeaders) { _inputRequest.Headers.Add(inputHeader); } _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert: Then The Mapped Request Has Each Header _mappedRequest.Headers.Count().ShouldBe(_inputHeaders.Count); foreach (var header in _mappedRequest.Headers) { var inputHeader = _inputHeaders.First(h => h.Key == header.Key); inputHeader.ShouldNotBe(default); inputHeader.Value.Count.ShouldBe(header.Value.Count()); foreach (var inputHeaderValue in inputHeader.Value) { Assert.Contains(header.Value, v => v == inputHeaderValue); } } } [Fact] public void Should_handle_no_headers() { // Arrange _inputRequest.Headers.Clear(); _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert _mappedRequest.Headers.Count().ShouldBe(0); } [Theory] [Trait("PR", "1972")] [InlineData("GET")] [InlineData("POST")] public async Task Should_map_content(string method) { // Arrange GivenTheInputRequestHasContent("This is my content"); _inputRequest.Method = method; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert await ThenTheMappedRequestHasContent("This is my content"); ThenTheMappedRequestHasContentLength("This is my content".Length); } [Fact] [Trait("PR", "1972")] public async Task Should_map_chucked_content() { // Arrange GivenTheInputRequestHasChunkedContent("This", " is my content"); _inputRequest.Method = "POST"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert await ThenTheMappedRequestHasContent("This is my content"); _mappedRequest.Headers.TryGetValues(HeaderNames.ContentLength, out _).ShouldBeFalse(); // ThenTheMappedRequestHasNoContentLength } [Fact] [Trait("PR", "1972")] public async Task Should_map_empty_content() { // Arrange GivenTheInputRequestHasContent(""); _inputRequest.Method = "POST"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert await ThenTheMappedRequestHasContent(""); ThenTheMappedRequestHasContentLength(0); } [Fact] [Trait("PR", "1972")] public async Task Should_map_empty_chucked_content() { // Arrange GivenTheInputRequestHasChunkedContent(); _inputRequest.Method = "POST"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert await ThenTheMappedRequestHasContent(""); _mappedRequest.Headers.TryGetValues(HeaderNames.ContentLength, out _).ShouldBeFalse(); // ThenTheMappedRequestHasNoContentLength } [Fact] public void Should_handle_no_content() { // Arrange _inputRequest.Body = null!; _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert _mappedRequest.Content.ShouldBeNull(); } [Fact] public void Should_handle_no_content_type() { // Arrange _inputRequest.ContentType = null; _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert _mappedRequest.Content.ShouldBeNull(); } [Fact] public void Should_handle_no_content_length() { // Arrange _inputRequest.ContentLength = null; _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert _mappedRequest.Content.ShouldBeNull(); } [Fact] public void Should_map_content_headers() { // Arrange var bytes = Encoding.UTF8.GetBytes("some md5"); var md5Bytes = MD5.HashData(bytes); GivenTheInputRequestHasContent("This is my content"); _inputRequest.ContentType = "application/json"; _inputRequest.Headers.Append("Content-Encoding", "gzip, compress"); _inputRequest.Headers.Append("Content-Language", "english"); _inputRequest.Headers.Append("Content-Location", "/my-receipts/38"); _inputRequest.Headers.Append("Content-Range", "bytes 1-2/*"); _inputRequest.Headers.Append("Content-Disposition", "inline"); var base64 = Convert.ToBase64String(md5Bytes); _inputRequest.Headers.Append("Content-MD5", base64); _inputRequest.Method = "GET"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert ThenTheMappedRequestHasContentTypeHeader("application/json"); Assert.NotNull(_mappedRequest.Content); _mappedRequest.Content.Headers.ContentEncoding.ToArray()[0].ShouldBe("gzip"); _mappedRequest.Content.Headers.ContentEncoding.ToArray()[1].ShouldBe("compress"); Assert.NotNull(_mappedRequest.Content); _mappedRequest.Content.Headers.ContentLanguage.First().ShouldBe("english"); Assert.NotNull(_mappedRequest.Content); Assert.NotNull(_mappedRequest.Content.Headers.ContentLocation); _mappedRequest.Content.Headers.ContentLocation.OriginalString.ShouldBe("/my-receipts/38"); Assert.NotNull(_mappedRequest.Content); _mappedRequest.Content.Headers.ContentMD5.ShouldBe(md5Bytes); Assert.NotNull(_mappedRequest.Content); Assert.NotNull(_mappedRequest.Content.Headers.ContentRange); _mappedRequest.Content.Headers.ContentRange.From.ShouldBe(1); _mappedRequest.Content.Headers.ContentRange.To.ShouldBe(2); Assert.NotNull(_mappedRequest.Content); Assert.NotNull(_mappedRequest.Content.Headers.ContentDisposition); _mappedRequest.Content.Headers.ContentDisposition.DispositionType.ShouldBe("inline"); // Assert: Then The Content-* Headers Are Not Added To Non Content Headers _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-Disposition"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-ContentMD5"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-ContentRange"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-ContentLanguage"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-ContentEncoding"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-ContentLocation"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-Length"); _mappedRequest.Headers.ShouldNotContain(x => x.Key == "Content-Type"); } [Fact] public void Should_not_add_content_headers() { // Arrange GivenTheInputRequestHasContent("This is my content"); _inputRequest.ContentType = "application/json"; _inputRequest.Method = "POST"; GivenTheInputRequestHasAValidUri(); GivenTheDownstreamRoute(); // Act WhenMapped(); // Assert ThenTheMappedRequestHasContentTypeHeader("application/json"); // Assert: Then The Other Content Type Headers Are Not Mapped Assert.NotNull(_mappedRequest.Content); _mappedRequest.Content.Headers.ContentDisposition.ShouldBeNull(); _mappedRequest.Content.Headers.ContentMD5.ShouldBeNull(); _mappedRequest.Content.Headers.ContentRange.ShouldBeNull(); _mappedRequest.Content.Headers.ContentLanguage.ShouldBeEmpty(); _mappedRequest.Content.Headers.ContentEncoding.ShouldBeEmpty(); _mappedRequest.Content.Headers.ContentLocation.ShouldBeNull(); } private void GivenTheDownstreamRoute() { _downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamHttpVersion(new Version("1.1")).Build(); } private void ThenTheMappedRequestHasContentTypeHeader(string expected) { Assert.NotNull(_mappedRequest.Content); Assert.NotNull(_mappedRequest.Content.Headers.ContentType); _mappedRequest.Content.Headers.ContentType.MediaType.ShouldBe(expected); } private void GivenTheInputRequestHasHost(string host) { _inputRequest.Host = new HostString(host); } private void GivenTheInputRequestHasPath(string path) { if (path != null) { _inputRequest.Path = path; } } private void GivenTheInputRequestHasQueryString(string querystring) { if (querystring != null) { _inputRequest.QueryString = new QueryString(querystring); } } private void GivenTheInputRequestHasAValidUri() { _inputRequest.Scheme = "http"; GivenTheInputRequestHasHost("www.google.com"); } private void GivenTheInputRequestHasContent(string content) { _inputRequest.ContentLength = content.Length; _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(content)); } private void GivenTheInputRequestHasChunkedContent(params string[] chunks) { // ASP.Net Core decodes chucked streams, so that the input request just sees the decoded data // Because of that, we just give a stream with the concatenated chunks to the test _inputRequest.Body = new MemoryStream(Encoding.UTF8.GetBytes(string.Join("", chunks))); _inputRequest.Headers.TransferEncoding = "chunked"; } private void WhenMapped() { _mappedRequest = _requestMapper.Map(_inputRequest, _downstreamRoute); } private async Task ThenTheMappedRequestHasContent(string expectedContent) { Assert.NotNull(_mappedRequest.Content); var contentAsString = await _mappedRequest.Content.ReadAsStringAsync(); contentAsString.ShouldBe(expectedContent); } private void ThenTheMappedRequestHasContentLength(long expectedLength) { Assert.NotNull(_mappedRequest.Content); _mappedRequest.Content.Headers.ContentLength.ShouldBe(expectedLength); } } ================================================ FILE: test/Ocelot.UnitTests/Request/Mapper/StreamHttpContentTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Request.Mapper; using System.Reflection; using System.Text; namespace Ocelot.UnitTests.Request.Mapper; public class StreamHttpContentTests { private readonly DefaultHttpContext _httpContext; private const string PayLoad = "[{\"_id\":\"65416ef7eafdf7953c4d7319\",\"index\":0,\"guid\":\"254b515d-0569-494d-9bc8-e21c8bd0365e\",\"isActive\":false,\"balance\":\"$1,225.59\",\"picture\":\"http://placehold.it/32x32\",\"age\":26,\"eyeColor\":\"blue\",\"name\":\"FayHatfield\",\"gender\":\"female\",\"company\":\"VIASIA\",\"email\":\"fayhatfield@viasia.com\",\"phone\":\"+1(970)416-2792\",\"address\":\"768MontroseAvenue,Mansfield,NewMexico,8890\",\"about\":\"Duisoccaecatdoloreeiusmoddoipsummollitaliquipnostrudqui.Cillumdoexercitationexercitationexcepteurincididuntadipisicingminimconsecteturofficiaanimdoloreincididuntlaborealiqua.Tempordoloreirurecillumadnullasuntoccaecatsitnulladosit.Sitnostrudullamcolaborisvelitvelitetofficiasitenimipsumaute.\\r\\n\",\"registered\":\"2023-07-03T03:10:08-02:00\",\"latitude\":0.117661,\"longitude\":-65.570177,\"tags\":[\"Lorem\",\"consequat\",\"consectetur\",\"pariatur\",\"fugiat\",\"est\",\"mollit\"],\"friends\":[{\"id\":0,\"name\":\"LynetteMelendez\"},{\"id\":1,\"name\":\"DrakeMay\"},{\"id\":2,\"name\":\"JenningsConrad\"}],\"greeting\":\"Hello,FayHatfield!Youhave3unreadmessages.\",\"favoriteFruit\":\"apple\"}]"; public StreamHttpContentTests() { _httpContext = new DefaultHttpContext(); } [Fact] public async Task Copy_body_to_stream_and_stream_content_should_match_payload() { // Arrange var sut = StreamHttpContentFactory(); using var stream = new MemoryStream(); // Act await sut.CopyToAsync(stream, TestContext.Current.CancellationToken); // Assert stream.Position = 0; var result = Encoding.UTF8.GetString(stream.ToArray()); result.ShouldBe(PayLoad); } [Fact] public async Task Copy_body_to_stream_with_unknown_length_and_stream_content_should_match_payload() { // Arrange var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); // Act await CopyAsyncTest( new StreamHttpContent(_httpContext), new object[] { inputStream, outputStream, StreamHttpContent.UnknownLength, false, CancellationToken.None }); // Assert inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); result.ShouldBe(PayLoad); } [Fact] public async Task Copy_body_to_stream_with_body_length_and_stream_content_should_match_payload() { // Arrange var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); // Act await CopyAsyncTest( new StreamHttpContent(_httpContext), new object[] { inputStream, outputStream, bytes.Length, false, CancellationToken.None }); // Assert inputStream.Position = 0; outputStream.Position = 0; var result = Encoding.UTF8.GetString(outputStream.ToArray()); result.ShouldBe(PayLoad); } [Fact] public async Task Should_throw_if_passed_body_length_does_not_match_real_body_length() { // Arrange var bytes = Encoding.UTF8.GetBytes(PayLoad); using var inputStream = new MemoryStream(bytes); using var outputStream = new MemoryStream(); // Act, Assert await Assert.ThrowsAsync(async () => await CopyAsyncTest( new StreamHttpContent(_httpContext), new object[] { inputStream, outputStream, 10, false, CancellationToken.None })); } private StreamHttpContent StreamHttpContentFactory() { var bytes = Encoding.UTF8.GetBytes(PayLoad); _httpContext.Request.Body = new MemoryStream(bytes); return new StreamHttpContent(_httpContext); } private static async Task CopyAsyncTest(StreamHttpContent streamHttpContent, object[] parameters) { var bindingAttr = BindingFlags.NonPublic | BindingFlags.Static; var method = typeof(StreamHttpContent).GetMethod("CopyAsync", bindingAttr) ?? throw new Exception("Could not find CopyAsync"); var task = (Task)method.Invoke(streamHttpContent, parameters) ?? throw new Exception("Could not invoke CopyAsync"); await task.ConfigureAwait(false); } } ================================================ FILE: test/Ocelot.UnitTests/RequestId/RequestIdMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.RequestId.Middleware; using Ocelot.Responses; namespace Ocelot.UnitTests.RequestId; public class RequestIdMiddlewareTests : UnitTest { private readonly HttpRequestMessage _downstreamRequest; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly RequestIdMiddleware _middleware; private readonly RequestDelegate _next; private readonly Mock _repo; private readonly DefaultHttpContext _httpContext; public RequestIdMiddlewareTests() { _httpContext = new DefaultHttpContext(); _downstreamRequest = new HttpRequestMessage(HttpMethod.Get, "http://test.com"); _repo = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => { _httpContext.Response.Headers.Append("LSRequestId", _httpContext.TraceIdentifier); return Task.CompletedTask; }; _middleware = new RequestIdMiddleware(_next, _loggerFactory.Object, _repo.Object); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(_downstreamRequest)); } [Fact] public async Task Should_pass_down_request_id_from_upstream_request() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(["Get"]) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); GivenTheDownStreamRouteIs(downstreamRoute); GivenThereIsNoGlobalRequestId(); _httpContext.Request.Headers.TryAdd("LSRequestId", requestId); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheTraceIdIs(requestId); } [Fact] public async Task Should_add_request_id_when_not_on_upstream_request() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(["Get"]) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); GivenTheDownStreamRouteIs(downstreamRoute); GivenThereIsNoGlobalRequestId(); // Act await _middleware.Invoke(_httpContext); // Assert: Then The TraceId Is Anything _httpContext.Response.Headers.TryGetValue("LSRequestId", out var value); value.First().ShouldNotBeNullOrEmpty(); } [Fact] public async Task Should_add_request_id_scoped_repo_for_logging_later() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); GivenTheDownStreamRouteIs(downstreamRoute); GivenThereIsNoGlobalRequestId(); _httpContext.Request.Headers.TryAdd("LSRequestId", requestId); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheTraceIdIs(requestId); _repo.Verify(x => x.Add("RequestId", requestId), Times.Once); } [Fact] public async Task Should_update_request_id_scoped_repo_for_logging_later() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(["Get"]) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); var requestId = Guid.NewGuid().ToString(); GivenTheDownStreamRouteIs(downstreamRoute); GivenTheRequestIdWasSetGlobally(); _httpContext.Request.Headers.TryAdd("LSRequestId", requestId); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheTraceIdIs(requestId); _repo.Verify(x => x.Update("RequestId", requestId), Times.Once); } [Fact] public async Task Should_not_update_if_global_request_id_is_same_as_re_route_request_id() { // Arrange var route = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") .WithRequestIdKey("LSRequestId") .WithUpstreamHttpMethod(new List { "Get" }) .Build(); var downstreamRoute = new DownstreamRouteHolder( new List(), new Route(route, HttpMethod.Get)); var requestId = "alreadyset"; GivenTheDownStreamRouteIs(downstreamRoute); GivenTheRequestIdWasSetGlobally(); _httpContext.Request.Headers.TryAdd("LSRequestId", requestId); // Act await _middleware.Invoke(_httpContext); // Assert ThenTheTraceIdIs(requestId); _repo.Verify(x => x.Update("RequestId", requestId), Times.Never); } private void GivenThereIsNoGlobalRequestId() { _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse(null)); } private void GivenTheRequestIdWasSetGlobally() { _repo.Setup(x => x.Get("RequestId")).Returns(new OkResponse("alreadyset")); } private void GivenTheDownStreamRouteIs(DownstreamRouteHolder downstreamRoute) { _httpContext.Items.UpsertTemplatePlaceholderNameAndValues(downstreamRoute.TemplatePlaceholderNameAndValues); _httpContext.Items.UpsertDownstreamRoute(downstreamRoute.Route.DownstreamRoute[0]); } private void ThenTheTraceIdIs(string expected) { _httpContext.Response.Headers.TryGetValue("LSRequestId", out var value); value.First().ShouldBe(expected); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/DelegatingHandlerFactoryTests.cs ================================================ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.QualityOfService; using Ocelot.Requester; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Requester; public class DelegatingHandlerFactoryTests : UnitTest { private DelegatingHandlerFactory _factory; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly Mock _qosFactory; private readonly Mock _tracingFactory; private readonly Mock> _optionsMonitor; private IServiceProvider _serviceProvider; private readonly IServiceCollection _services; private readonly QosDelegatingHandlerDelegate _qosDelegate; public DelegatingHandlerFactoryTests() { _qosDelegate = (a, b, c) => new FakeQoSHandler(); _tracingFactory = new Mock(); _qosFactory = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _services = new ServiceCollection(); _services.AddSingleton(_qosDelegate); _optionsMonitor = new(); _optionsMonitor.SetupGet(x => x.CurrentValue).Returns(new FileConfiguration()); _services.AddSingleton(_optionsMonitor.Object); } protected static FileHttpHandlerOptions GivenHandlerOptions => new() { AllowAutoRedirect = true, UseCookieContainer = true, UseProxy = true, UseTracing = true, }; private static QoSOptions GivenQoS() => new(1, 1) { Timeout = 1, }; [Fact] public void Should_follow_ordering_add_specifics() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", "FakeDelegatingHandlerTwo", }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(6); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); result.ThenHandlerAtPositionIs(4); result.ThenHandlerAtPositionIs(5); } [Fact] public void Should_follow_ordering_order_specifics_and_globals() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithDelegatingHandlers(new List { "FakeDelegatingHandlerTwo", "FakeDelegatingHandler", "FakeDelegatingHandlerFour", }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(6); result.ThenHandlerAtPositionIs(0); //first because global not in config result.ThenHandlerAtPositionIs(1); //first from config result.ThenHandlerAtPositionIs(2); //second from config result.ThenHandlerAtPositionIs(3); //third from config (global) result.ThenHandlerAtPositionIs(4); result.ThenHandlerAtPositionIs(5); } [Fact] public void Should_follow_ordering_order_specifics() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithDelegatingHandlers(new List { "FakeDelegatingHandlerTwo", "FakeDelegatingHandler", }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(6); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); result.ThenHandlerAtPositionIs(4); result.ThenHandlerAtPositionIs(5); } [Fact] public void Should_follow_ordering_order_and_only_add_specifics_in_config() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(5); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); result.ThenHandlerAtPositionIs(4); } [Fact] public void Should_follow_ordering_dont_add_specifics() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(4); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); } [Fact] public void Should_apply_re_route_specific() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(new()) .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false }) .WithDelegatingHandlers(new List { "FakeDelegatingHandler", "FakeDelegatingHandlerTwo", }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(2); result.ThenTheDelegatesAreAddedCorrectly(); } [Fact] public void Should_all_from_all_routes_provider_and_qos() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(3); result.ThenTheDelegatesAreAddedCorrectly(); result.ThenItIsQosHandler(2); } [Fact] public void Should_return_provider_with_no_delegates() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(new()) .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheServiceProviderReturnsNothing(); // Act var result = WhenIGet(route); // Assert: Then No Delegates Are In The Provider result.ShouldNotBeNull().ShouldBeEmpty(); } [Fact] public void Should_return_provider_with_qos_delegate() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheServiceProviderReturnsNothing(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(1); result.ThenItIsQosHandler(0); } [Fact] public void Should_return_provider_with_qos_delegate_when_timeout_value_set() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(new(timeout: 1)) .WithHttpHandlerOptions(new(GivenHandlerOptions) { UseTracing = false }) .WithLoadBalancerKey(string.Empty) .Build(); GivenTheQosFactoryReturns(new FakeQoSHandler()); GivenTheServiceProviderReturnsNothing(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(1); result.ThenItIsQosHandler(0); } [Fact] public void Should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_error() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithLoadBalancerKey(string.Empty) .Build(); _qosFactory.Setup(x => x.Get(It.IsAny())) .Returns(new ErrorResponse(new AnyError())); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(4); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); ThenTheWarningIsLogged(route); } [Fact] public void Should_log_error_and_return_no_qos_provider_delegate_when_qos_factory_returns_null() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(GivenQoS()) .WithHttpHandlerOptions(new(GivenHandlerOptions)) .WithLoadBalancerKey(string.Empty) .Build(); _qosFactory.Setup(x => x.Get(It.IsAny())) .Returns((ErrorResponse)null); GivenTheTracingFactoryReturns(); GivenTheServiceProviderReturnsGlobalDelegatingHandlers(); GivenTheServiceProviderReturnsSpecificDelegatingHandlers(); // Act var result = WhenIGet(route); // Assert result.ThenThereIsDelegatesInProvider(4); result.ThenHandlerAtPositionIs(0); result.ThenHandlerAtPositionIs(1); result.ThenHandlerAtPositionIs(2); result.ThenHandlerAtPositionIs(3); ThenTheWarningIsLogged(route); } private void ThenTheWarningIsLogged(DownstreamRoute route) { _logger.Verify(x => x.LogWarning(It.Is>( y => y.Invoke() == $"Route '{route.Name()}' specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!")), Times.Once); } private void GivenTheTracingFactoryReturns() { _tracingFactory .Setup(x => x.Get()) .Returns(new FakeTracingHandler()); } private void GivenTheServiceProviderReturnsGlobalDelegatingHandlers() where TOne : DelegatingHandler where TTwo : DelegatingHandler { _services.AddTransient(); _services.AddTransient(s => { var service = s.GetService(); return new GlobalDelegatingHandler(service); }); _services.AddTransient(); _services.AddTransient(s => { var service = s.GetService(); return new GlobalDelegatingHandler(service); }); } private void GivenTheServiceProviderReturnsSpecificDelegatingHandlers() where TOne : DelegatingHandler where TTwo : DelegatingHandler { _services.AddTransient(); _services.AddTransient(); } private void GivenTheServiceProviderReturnsNothing() { _serviceProvider = _services.BuildServiceProvider(true); } private void GivenTheQosFactoryReturns(DelegatingHandler handler) { _qosFactory .Setup(x => x.Get(It.IsAny())) .Returns(new OkResponse(handler)); } private List WhenIGet(DownstreamRoute route) { _serviceProvider = _services.BuildServiceProvider(true); _factory = new DelegatingHandlerFactory(_tracingFactory.Object, _qosFactory.Object, _serviceProvider, _loggerFactory.Object); return _factory.Get(route); } } internal static class ListExtensions { public static void ThenItIsQosHandler(this List result, int i) { result[i].ShouldNotBeNull().ShouldBeOfType(); } public static void ThenTheDelegatesAreAddedCorrectly(this List result) { var handler = (FakeDelegatingHandler)result[0].ShouldNotBeNull(); handler.Order.ShouldBe(1); var handlerTwo = (FakeDelegatingHandlerTwo)result[1].ShouldNotBeNull(); handlerTwo.Order.ShouldBe(2); } public static void ThenThereIsDelegatesInProvider(this List result, int count) { result.ShouldNotBeNull().Count.ShouldBe(count); } public static void ThenHandlerAtPositionIs(this List result, int pos) where T : DelegatingHandler { result[pos].ShouldNotBeNull().ShouldBeOfType(); } } internal class FakeTracingHandler : DelegatingHandler, ITracingHandler { } internal class FakeQoSHandler : DelegatingHandler { } ================================================ FILE: test/Ocelot.UnitTests/Requester/FakeDelegatingHandler.cs ================================================ namespace Ocelot.UnitTests.Requester; public class FakeDelegatingHandler : DelegatingHandler { public FakeDelegatingHandler() => Order = 1; public FakeDelegatingHandler(int order) => Order = order; public int Order { get; } public DateTime TimeCalled { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { TimeCalled = DateTime.Now; return Task.FromResult(new HttpResponseMessage()); } } public class FakeDelegatingHandlerThree : DelegatingHandler { public FakeDelegatingHandlerThree() => Order = 3; public int Order { get; } public DateTime TimeCalled { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { TimeCalled = DateTime.Now; return Task.FromResult(new HttpResponseMessage()); } } public class FakeDelegatingHandlerFour : DelegatingHandler { public FakeDelegatingHandlerFour() => Order = 4; public int Order { get; } public DateTime TimeCalled { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { TimeCalled = DateTime.Now; return Task.FromResult(new HttpResponseMessage()); } } public class FakeDelegatingHandlerTwo : DelegatingHandler { public FakeDelegatingHandlerTwo() => Order = 2; public int Order { get; } public DateTime TimeCalled { get; private set; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { TimeCalled = DateTime.Now; return Task.FromResult(new HttpResponseMessage()); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/HttpExceptionToErrorMapperTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Errors; using Ocelot.Request.Mapper; using Ocelot.Requester; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Requester; [Trait("Feat", "562")] // https://github.com/ThreeMammals/Ocelot/pull/562 [Trait("Release", "10.0.3")] // https://github.com/ThreeMammals/Ocelot/releases/tag/10.0.3 public class HttpExceptionToErrorMapperTests { private HttpExceptionToErrorMapper _mapper; private readonly ServiceCollection _services; public HttpExceptionToErrorMapperTests() { _services = new ServiceCollection(); var provider = _services.BuildServiceProvider(true); _mapper = new HttpExceptionToErrorMapper(provider); } [Fact] public void Should_return_default_error_because_mappers_are_null() { // Arrange, Act var error = _mapper.Map(new Exception()); // Assert error.ShouldBeOfType(); } [Fact] [Trait("PR", "902")] // https://github.com/ThreeMammals/Ocelot/pull/902 public void Should_return_request_canceled() { // Arrange, Act var error = _mapper.Map(new OperationCanceledException()); // Assert error.ShouldBeOfType(); } [Fact] public void Should_return_ConnectionToDownstreamServiceError() { // Arrange, Act var error = _mapper.Map(new HttpRequestException()); // Assert error.ShouldBeOfType(); } private class SomeException : OperationCanceledException { } [Fact] public void Should_return_request_canceled_for_subtype() { // Arrange, Act var error = _mapper.Map(new SomeException()); // Assert error.ShouldBeOfType(); } [Fact] public void Should_return_error_from_mapper() { // Arrange IDictionary> errorMapping = new Dictionary> { {typeof(TaskCanceledException), e => new AnyError()}, }; _services.AddSingleton(errorMapping); var provider = _services.BuildServiceProvider(true); _mapper = new HttpExceptionToErrorMapper(provider); // Act var error = _mapper.Map(new TaskCanceledException()); // Assert error.ShouldBeOfType(); } [Fact] [Trait("PR", "1824")] // https://github.com/ThreeMammals/Ocelot/pull/1824 public void Map_TimeoutException_To_RequestTimedOutError() { // Arrange var ex = new TimeoutException("test"); // Act var error = _mapper.Map(ex); // Assert Assert.IsType(error); Assert.Equal(25, (int)error.Code); Assert.Equal(503, error.HttpStatusCode); Assert.Equal("Timeout making http request, exception: System.TimeoutException: test", error.Message); } [Fact] [Trait("Bug", "749")] // https://github.com/ThreeMammals/Ocelot/issues/749 [Trait("PR", "1769")] // https://github.com/ThreeMammals/Ocelot/pull/1769 public void Map_BadHttpRequestException_To_PayloadTooLargeError() { // Arrange var inner = new BadHttpRequestException("test-inner", 413); var ex = new HttpRequestException("test", inner); // Act var error = _mapper.Map(ex); // Assert Assert.IsType(error); Assert.Equal(41, (int)error.Code); Assert.Equal(413, error.HttpStatusCode); Assert.Equal("test", error.Message); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Requester; using Ocelot.Requester.Middleware; using Ocelot.Responses; using Ocelot.UnitTests.Responder; namespace Ocelot.UnitTests.Requester; public class HttpRequesterMiddlewareTests : UnitTest { private readonly Mock _requester; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly HttpRequesterMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public HttpRequesterMiddlewareTests() { _httpContext = new DefaultHttpContext(); _requester = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new HttpRequesterMiddleware(_next, _loggerFactory.Object, _requester.Object); _httpContext.Items.UpsertDownstreamRoute(new DownstreamRouteBuilder().Build()); // Given The Request Is } [Fact] public async Task Should_call_services_correctly() { // Arrange var response = GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(HttpStatusCode.OK))); // Act await _middleware.Invoke(_httpContext); // Assert InformationIsLogged(); // Assert: Then The Downstream Response Is Set foreach (var httpResponseHeader in response.Data.Headers) { if (_httpContext.Items.DownstreamResponse().Headers.Any(x => x.Key == httpResponseHeader.Key)) { throw new Exception("Header in response not in downstreamresponse headers"); } } _httpContext.Items.DownstreamResponse().Content.ShouldBe(response.Data.Content); _httpContext.Items.DownstreamResponse().StatusCode.ShouldBe(response.Data.StatusCode); } [Fact] public async Task Should_set_error() { // Arrange GivenTheRequesterReturns(new ErrorResponse(new AnyError())); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); } [Fact] public async Task Should_log_downstream_internal_server_error() { // Arrange GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(HttpStatusCode.InternalServerError))); // Act await _middleware.Invoke(_httpContext); // Assert WarningIsLogged(); } [Theory] [Trait("Bug", "1953")] [InlineData(HttpStatusCode.OK)] [InlineData(HttpStatusCode.PermanentRedirect)] public async Task Should_LogInformation_when_status_is_less_than_BadRequest(HttpStatusCode status) { // Arrange GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(status))); // Act await _middleware.Invoke(_httpContext); // Assert InformationIsLogged(); } [Theory] [Trait("Bug", "1953")] [InlineData(HttpStatusCode.BadRequest)] [InlineData(HttpStatusCode.NotFound)] public async Task Should_LogWarning_when_status_is_BadRequest_or_greater(HttpStatusCode status) { // Arrange GivenTheRequesterReturns(new OkResponse(new HttpResponseMessage(status))); // Act await _middleware.Invoke(_httpContext); // Assert WarningIsLogged(); } private Response GivenTheRequesterReturns(Response response) { _requester.Setup(x => x.GetResponse(It.IsAny())) .ReturnsAsync(response); return response; } private void WarningIsLogged() { _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once); } private void InformationIsLogged() { _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Once); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/MessageInvokerCacheKeyTests.cs ================================================ using Ocelot.Configuration; using Ocelot.Configuration.Builder; using static Ocelot.Requester.MessageInvokerPool; namespace Ocelot.UnitTests.Requester; public class MessageInvokerCacheKeyTests { [Fact] public void Equals_Object() { var route1 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r1", 0, false, "/r1")) .Build(); var route2 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r2", 0, false, "/r2")) .Build(); var key1 = new MessageInvokerCacheKey(route1); object key2 = new MessageInvokerCacheKey(route2); // Act, Assert 0: If different types bool isDiffTypes = key1.Equals(new DownstreamRouteBuilder()); Assert.False(isDiffTypes); // Act, Assert 1 bool isEqual = key1.Equals(key2); Assert.False(isEqual); // Arrange 2 var route3 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r1", 0, false, "/r3")) .Build(); object key3 = new MessageInvokerCacheKey(route3); // Act, Assert 1 isEqual = key1.Equals(key3); Assert.False(isEqual); // Arrange 3 var route4 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r1", 0, false, "/r1")) .Build(); object key4 = new MessageInvokerCacheKey(route4); // Act, Assert 1 isEqual = key1.Equals(key4); // Assert.True(isEqual); // O-ho-ho! :( Assert.False(isEqual); // actually objects are different :( // Life hack for Guillaume ;)) LoL // This method has taken from source code of the public sealed partial class ObjectEqualityComparer : EqualityComparer // Link to source: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/EqualityComparer.cs#L186-L198 static bool EqualsForGui(DownstreamRoute x, DownstreamRoute y) { if (x != null) { if (y != null) return x.Equals(y); // object.Equals(object) -> https://github.com/dotnet/runtime/blob/0621e649bd084cb0dfd1f2e627538e7d9aa9e211/src/libraries/System.Private.CoreLib/src/System/Object.cs#L45-L64 return false; } if (y != null) return false; return true; } // Two object with absolutely identical internal state var d1 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); var d2 = new DownstreamRoute(default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default, default); // Act for Gui :) This will be a gift for Gui for Christmas! LOL bool happyStart = d1.Equals(d2); // object.Equals(object) bool happyEnd = EqualsForGui(d1, d2); // also uses object.Equals(object) // Assert Bingo! Assert.Equal(happyStart, happyEnd); Assert.True(EqualsForGui(d1, d1)); // it is true because same reference was compared to itself by object.Equals(object), so this is default implementation of object.Equals(object) // Assert.True(happyEnd); // but it is False actually Assert.False(happyEnd); // No happy end by Gui... } [Fact] public void Equality_Operator() { var route1 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r1", 0, false, "/r1")) .Build(); var route2 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r2", 0, false, "/r2")) .Build(); var key1 = new MessageInvokerCacheKey(route1); var key2 = new MessageInvokerCacheKey(route2); // Act bool isEqual = key1 == key2; // Assert Assert.False(isEqual); } [Fact] public void Inequality_Operator() { var route1 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r1", 0, false, "/r1")) .Build(); var route2 = new DownstreamRouteBuilder() .WithUpstreamPathTemplate(new("/r2", 0, false, "/r2")) .Build(); var key1 = new MessageInvokerCacheKey(route1); var key2 = new MessageInvokerCacheKey(route2); // Act bool notEqual = key1 != key2; // Assert Assert.True(notEqual); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/MessageInvokerHttpRequesterTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester; using Ocelot.Responses; namespace Ocelot.UnitTests.Requester; public class MessageInvokerHttpRequesterTests { private readonly Mock _loggerFactoryMock; private readonly Mock _loggerMock; private readonly Mock _messageInvokerPoolMock; private readonly Mock _mapperMock; private readonly Mock _messageInvokerMock; private readonly MessageInvokerHttpRequester _sut; public MessageInvokerHttpRequesterTests() { _loggerFactoryMock = new Mock(); _loggerMock = new Mock(); _loggerFactoryMock.Setup(f => f.CreateLogger()) .Returns(_loggerMock.Object); _messageInvokerPoolMock = new Mock(); _mapperMock = new Mock(); _messageInvokerMock = new Mock(new HttpClientHandler()); _sut = new MessageInvokerHttpRequester( _loggerFactoryMock.Object, _messageInvokerPoolMock.Object, _mapperMock.Object); } private static DefaultHttpContext CreateHttpContext() { var context = new DefaultHttpContext(); // Ocelot adds DownstreamRequest and DownstreamRoute into Items var downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test")); var downstreamRoute = new DownstreamRouteBuilder().Build(); context.Items.UpsertDownstreamRequest(downstreamRequest); context.Items.UpsertDownstreamRoute(downstreamRoute); return context; } [Fact] public async Task GetResponse_ReturnsOkResponse_WhenMessageInvokerSucceeds() { // Arrange var context = CreateHttpContext(); var expectedResponse = new HttpResponseMessage(HttpStatusCode.OK); _messageInvokerPoolMock .Setup(p => p.Get(It.IsAny())) .Returns(_messageInvokerMock.Object); _messageInvokerMock .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(expectedResponse); // Act var result = await _sut.GetResponse(context); // Assert var okResponse = Assert.IsType>(result); Assert.Equal(expectedResponse, okResponse.Data); } [Fact] public async Task GetResponse_ReturnsErrorResponse_WhenMessageInvokerThrows() { // Arrange var context = CreateHttpContext(); var exception = new InvalidOperationException("Test exception"); var expectedError = new UnableToCompleteRequestError(new("mapped-error")); _messageInvokerPoolMock .Setup(p => p.Get(It.IsAny())) .Returns(_messageInvokerMock.Object); _messageInvokerMock .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) .ThrowsAsync(exception); _mapperMock.Setup(m => m.Map(exception)).Returns(expectedError); // Act var result = await _sut.GetResponse(context); // Assert var errorResponse = Assert.IsType>(result); Assert.Contains(expectedError, errorResponse.Errors); } [Fact] public void Constructor_CreatesInstance_WhenDependenciesAreValid() { // Arrange var loggerFactoryMock = new Mock(); var loggerMock = new Mock(); loggerFactoryMock.Setup(f => f.CreateLogger()) .Returns(loggerMock.Object); var messageInvokerPoolMock = new Mock(); var mapperMock = new Mock(); // Act var sut = new MessageInvokerHttpRequester( loggerFactoryMock.Object, messageInvokerPoolMock.Object, mapperMock.Object); // Assert Assert.NotNull(sut); // Verify that the logger factory was used to create a logger loggerFactoryMock.Verify(f => f.CreateLogger(), Times.Once); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenLoggerFactoryIsNull() { // Arrange var messageInvokerPoolMock = new Mock(); var mapperMock = new Mock(); // Act & Assert Assert.Throws(() => new MessageInvokerHttpRequester(null!, messageInvokerPoolMock.Object, mapperMock.Object)); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenMessageInvokerPoolIsNull() { // Arrange var loggerFactoryMock = new Mock(); var mapperMock = new Mock(); // Act & Assert Assert.Throws(() => new MessageInvokerHttpRequester(loggerFactoryMock.Object, null!, mapperMock.Object)); } [Fact] public void Constructor_ThrowsArgumentNullException_WhenMapperIsNull() { // Arrange var loggerFactoryMock = new Mock(); var messageInvokerPoolMock = new Mock(); // Act & Assert Assert.Throws(() => new MessageInvokerHttpRequester(loggerFactoryMock.Object, messageInvokerPoolMock.Object, null!)); } } ================================================ FILE: test/Ocelot.UnitTests/Requester/MessageInvokerPoolTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Configuration.File; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Requester; using System.Collections.Concurrent; using System.Diagnostics; using System.Net.Security; using System.Reflection; using Xunit.Sdk; namespace Ocelot.UnitTests.Requester; public class MessageInvokerPoolTests : MessageInvokerPoolBase { private DownstreamRoute _downstreamRoute1; private DownstreamRoute _downstreamRoute2; [Fact] [Trait("PR", "1824")] public void If_calling_the_same_downstream_route_twice_should_return_the_same_message_invoker() { // Arrange _downstreamRoute1 = DownstreamRouteFactory("/super-test"); AndAHandlerFactory(); GivenAMessageInvokerPool(); // Act var firstInvoker = _pool.Get(_downstreamRoute1); var secondInvoker = _pool.Get(_downstreamRoute1); // Assert Assert.Equal(firstInvoker, secondInvoker); } [Fact] [Trait("PR", "1824")] public void If_calling_two_different_downstream_routes_should_return_different_message_invokers() { // Arrange _downstreamRoute1 = DownstreamRouteFactory("/super-test"); _downstreamRoute2 = DownstreamRouteFactory("/super-test"); AndAHandlerFactory(); GivenAMessageInvokerPool(); // Act var firstInvoker = _pool.Get(_downstreamRoute1); var secondInvoker = _pool.Get(_downstreamRoute2); // Assert Assert.NotEqual(firstInvoker, secondInvoker); } [Fact] [Trait("PR", "1824")] public async Task If_two_delegating_handlers_are_defined_then_these_should_be_call_in_order() { // Arrange var fakeOne = new FakeDelegatingHandler(); var fakeTwo = new FakeDelegatingHandler(); var handlers = new List { fakeOne, fakeTwo }; GivenTheFactoryReturns(handlers); _downstreamRoute1 = DownstreamRouteFactory("/super-test"); GivenAMessageInvokerPool(); var port = PortFinder.GetRandomPort(); GivenARequestWithAUrlAndMethod(_downstreamRoute1, $"http://localhost:{port}", HttpMethod.Get); // Act await WhenICallTheClient("http://www.bbc.co.uk"); // Assert ThenTheFakeAreHandledInOrder(fakeOne, fakeTwo); _response.ShouldNotBeNull(); } [Fact] [Trait("PR", "1824")] public async Task Should_log_if_ignoring_ssl_errors() { // Arrange var route = new DownstreamRouteBuilder() .WithQosOptions(new()) .WithHttpHandlerOptions(new() { UseProxy = true }) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithDangerousAcceptAnyServerCertificateValidator(true) // The test should pass without timeout definition -> implicit default timeout //.WithTimeout(DownstreamRoute.DefaultTimeoutSeconds) .Build(); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); var port = PortFinder.GetRandomPort(); GivenARequest(route, port); // Act await WhenICallTheClient("http://www.google.com/"); // Assert: Then the DangerousAcceptAnyServerCertificateValidator warning is logged _ocelotLogger.Verify( x => x.LogWarning(It.Is>(y => y.Invoke() == $"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute -> {_context.Items.DownstreamRoute().Name()}")), Times.Once); } #region PR 2073 [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(1)] [InlineData(3)] public void SendAsync_NoQosAndHasRouteTimeout_ThrowTimeoutExceptionAfterRouteTimeout(int routeTimeoutSeconds) { // Arrange var route = GivenRoute(null, routeTimeoutSeconds); GivenTheFactoryReturnsNothing(); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); var port = PortFinder.GetRandomPort(); GivenARequest(route, port); //// Act, Assert //int marginMs = 50; //var expected = TimeSpan.FromSeconds(routeTimeoutSeconds); //var watcher = await TestRetry.NoWaitAsync( // () => WhenICallTheClientWillThrowAfterTimeout(expected, marginMs *= 2)); // call up to 3 times with margins 100, 200, 400 //AssertTimeoutPrecisely(watcher, expected); // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert AssertTimeout(invoker, routeTimeoutSeconds); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(1, 2)] [InlineData(3, 4)] public void CreateMessageInvoker_QosTimeoutAndRouteOne_CreatedTimeoutDelegatingHandlerWithoutQosTimeout(int qosTimeout, int routeTimeout) { // Arrange var route = GivenRoute(qosTimeout, routeTimeout); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert var actual = AssertTimeout(invoker, routeTimeout); Assert.NotEqual(qosTimeout, (int)actual.TotalSeconds); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(1, 2, 2, 0, "")] // QoS timeout < route timeout [InlineData(3, 4, 4, 0, "")] // QoS timeout < route timeout [InlineData(2, 1, 4, 1, "Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (1000 ms) is shorter than the QoS Timeout (2000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 4000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!")] // QoS timeout > route timeout [InlineData(4, 3, 8, 1, "Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (3000 ms) is shorter than the QoS Timeout (4000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 8000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!")] // QoS timeout > route timeout [InlineData(5, 5, 10, 1, "Route '/' has Quality of Service settings (QoSOptions) enabled, but either the route Timeout or the QoS Timeout is misconfigured: specifically, the route Timeout (5000 ms) is equal to the QoS Timeout (5000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 10000 ms to the route Timeout. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!")] // QoS timeout == route timeout [InlineData(DownstreamRoute.DefTimeout + 1, null, 2 * (DownstreamRoute.DefTimeout + 1), 1, "Route '/' has Quality of Service settings (QoSOptions) enabled, but either the DownstreamRoute.DefaultTimeoutSeconds or the QoS Timeout is misconfigured: specifically, the DownstreamRoute.DefaultTimeoutSeconds (90000 ms) is shorter than the QoS Timeout (91000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 182000 ms to the route Timeout instead of using DownstreamRoute.DefaultTimeoutSeconds. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!")] // DefaultTimeoutSeconds as route timeout public void EnsureRouteTimeoutIsGreaterThanQosOne_QosTimeoutVsRouteOne_ExpectedRouteTimeoutOrDoubledQosTimeout(int qosTimeout, int? routeTimeout, int expectedSeconds, int loggedCount, string expectedMessage) { // Arrange var route = GivenRoute(qosTimeout, routeTimeout); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); Func fMsg = null; _ocelotLogger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(f => fMsg = f); // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert Assert.NotEqual(expectedSeconds, qosTimeout); AssertTimeout(invoker, expectedSeconds); _ocelotLogger.Verify(x => x.LogWarning(It.IsAny>()), Times.Exactly(loggedCount)); var message = fMsg?.Invoke() ?? string.Empty; Assert.Equal(expectedMessage, message); } [Theory] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] [InlineData(1, 2, "is shorter than")] [InlineData(2, 2, "is equal to")] [InlineData(3, 2, "is longer than")] public void EqualitySentence_ThreeCases(int left, int right, string expected) { // Arrange, Act var actual = MessageInvokerPool.EqualitySentence(left, right); // Assert Assert.Equal(expected, actual); } #endregion [Fact] public void CreateHandler_DangerousAcceptAnyServerCertificateValidatorIsTrue_InitializedRemoteCertificateValidationCallback() { // Arrange const bool DangerousAcceptAnyServerCertificateValidator = true; var route = GivenRoute(null, null, DangerousAcceptAnyServerCertificateValidator); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); Func fMsg = null; _ocelotLogger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(f => fMsg = f); // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert _ocelotLogger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var message = fMsg?.Invoke() ?? string.Empty; Assert.Equal("You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute -> /", message); var handler = AssertTimeoutDelegatingHandler(invoker); var baseHandler = handler.InnerHandler as SocketsHttpHandler; Assert.NotNull(baseHandler.CookieContainer); Assert.NotNull(baseHandler?.SslOptions?.RemoteCertificateValidationCallback); bool alwaysTrue = baseHandler.SslOptions.RemoteCertificateValidationCallback.Invoke(this, null, null, SslPolicyErrors.None); Assert.True(alwaysTrue); } [Fact] public void Clear_WithOneItem_HandlersPoolShouldBeEmpty() { // Arrange var route = GivenRoute(null, null); GivenTheFactoryReturns(new()); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); // Act, Assert 1 using var invoker = _pool.Get(_context.Items.DownstreamRoute()); Assert.NotNull(invoker); Type me = _pool.GetType(); var field = me.GetField("_handlersPool", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); var handlersPool = field.GetValue(_pool) as ConcurrentDictionary>; Assert.NotNull(handlersPool); Assert.NotEmpty(handlersPool); Assert.Single(handlersPool); // Act, Assert 2 _pool.Clear(); Assert.Empty(handlersPool); _ocelotLogger.Verify(x => x.LogWarning(It.IsAny>()), Times.Never()); } private void AndAHandlerFactory() => _handlerFactory = GetHandlerFactory(); private async Task WhenICallTheClient(string url) { var messageInvoker = _pool.Get(_context.Items.DownstreamRoute()); _response = await messageInvoker .SendAsync(new HttpRequestMessage(HttpMethod.Get, url), CancellationToken.None); } private static void ThenTheFakeAreHandledInOrder(FakeDelegatingHandler fakeOne, FakeDelegatingHandler fakeTwo) => fakeOne.TimeCalled.ShouldBeGreaterThan(fakeTwo.TimeCalled); private static Mock GetHandlerFactory() { var handlerFactory = new Mock(); handlerFactory.Setup(x => x.Get(It.IsAny())) .Returns(new List()); return handlerFactory; } private static DownstreamRoute DownstreamRouteFactory(string path) => new DownstreamRouteBuilder() .WithDownstreamPathTemplate(path) .WithQosOptions(new QoSOptions(new FileQoSOptions())) .WithLoadBalancerKey(string.Empty) .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue(string.Empty).Build()) .WithHttpHandlerOptions(new() { MaxConnectionsPerServer = 10, PooledConnectionLifeTime = TimeSpan.FromSeconds(120) }) .WithUpstreamHttpMethod(["Get"]) .Build(); [Collection(nameof(SequentialTests))] public sealed class Sequential : MessageInvokerPoolBase { [Fact] [Trait("Bug", "1833")] public void SendAsync_NoQosAndNoRouteTimeouts_ShouldTimeoutAfterDefaultSeconds() { // Arrange var route = GivenRoute(null, null); GivenTheFactoryReturnsNothing(); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); // Act, Assert DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // minimum possible try { //int marginMs = 50; //var expected = TimeSpan.FromSeconds(DownstreamRoute.LowTimeout); //var watcher = await TestRetry.NoWaitAsync( // () => WhenICallTheClientWillThrowAfterTimeout(expected, marginMs *= 2)); // call up to 3 times with margins 100, 200, 400 //AssertTimeoutPrecisely(watcher, expected); // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert AssertTimeout(invoker, DownstreamRoute.LowTimeout); } finally { DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; } } [Fact] [Trait("PR", "2073")] [Trait("Feat", "1314")] [Trait("Feat", "1869")] public void EnsureRouteTimeoutIsGreaterThanQosOne_RouteQosTimeoutIsGreaterThanRouteOne_EnsuredQos() { // Arrange var route = GivenRoute(DownstreamRoute.LowTimeout + 1, null); GivenTheFactoryReturnsNothing(); GivenAMessageInvokerPool(); GivenARequest(route, PortFinder.GetRandomPort()); Func fMsg = null; _ocelotLogger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(f => fMsg = f); // Act, Assert DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.LowTimeout; // minimum possible try { // Act using var invoker = _pool.Get(_context.Items.DownstreamRoute()); // Assert AssertTimeout(invoker, 8); // should have doubled QoS timeout _ocelotLogger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var message = fMsg?.Invoke() ?? string.Empty; Assert.Equal("Route '/' has Quality of Service settings (QoSOptions) enabled, but either the DownstreamRoute.DefaultTimeoutSeconds or the QoS Timeout is misconfigured: specifically, the DownstreamRoute.DefaultTimeoutSeconds (3000 ms) is shorter than the QoS Timeout (4000 ms). To mitigate potential request failures, logged errors, or unexpected behavior caused by Polly's timeout strategy, Ocelot auto-doubled the QoS Timeout and applied 8000 ms to the route Timeout instead of using DownstreamRoute.DefaultTimeoutSeconds. However, this adjustment does not guarantee correct Polly behavior. Therefore, it's essential to assign correct values to both timeouts as soon as possible!", message); } finally { DownstreamRoute.DefaultTimeoutSeconds = DownstreamRoute.DefTimeout; } } } } public class MessageInvokerPoolBase : UnitTest { protected Mock _handlerFactory; protected HttpResponseMessage _response; protected MessageInvokerPool _pool; protected readonly DefaultHttpContext _context = new(); protected readonly Mock _ocelotLogger = new(); protected readonly Mock _ocelotLoggerFactory = new(); public MessageInvokerPoolBase() { _ocelotLoggerFactory.Setup(x => x.CreateLogger()).Returns(_ocelotLogger.Object); } public static int Ms(int seconds) => 1000 * seconds; protected static DownstreamRoute GivenRoute(int? qosTimeout, int? routeTimeout, bool dangerousAcceptAnyServerCertificateValidator = false) { var qosOptions = new QoSOptions(qosTimeout.HasValue ? Ms(qosTimeout.Value) : null); // !!! var handlerOptions = new HttpHandlerOptions() { MaxConnectionsPerServer = int.MaxValue, UseCookieContainer = true, }; var route = new DownstreamRouteBuilder() .WithQosOptions(qosOptions) .WithHttpHandlerOptions(handlerOptions) .WithTimeout(routeTimeout) // !!! .WithUpstreamPathTemplate(new("/", 0, false, "/")) .WithDangerousAcceptAnyServerCertificateValidator(dangerousAcceptAnyServerCertificateValidator) .Build(); return route; } protected void GivenTheFactoryReturnsNothing() { var nothing = new List(); GivenTheFactoryReturns(nothing); } protected void GivenTheFactoryReturns(List handlers) { _handlerFactory = new Mock(); _handlerFactory.Setup(x => x.Get(It.IsAny())) .Returns(handlers); } protected void GivenAMessageInvokerPool() => _pool = new MessageInvokerPool(_handlerFactory.Object, _ocelotLoggerFactory.Object); protected void GivenARequest(DownstreamRoute downstream, int port) => GivenARequestWithAUrlAndMethod(downstream, Url(port), HttpMethod.Get); protected void GivenARequestWithAUrlAndMethod(DownstreamRoute downstream, string url, HttpMethod method) { _context.Items.UpsertDownstreamRoute(downstream); _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage { RequestUri = new Uri(url), Method = method })); } protected async Task WhenICallTheClientWillThrowAfterTimeout(TimeSpan timeout, int marginMilliseconds) { var messageInvoker = _pool.Get(_context.Items.DownstreamRoute()); var stopwatch = new Stopwatch(); stopwatch.Start(); try { _response = await messageInvoker .SendAsync(new HttpRequestMessage(HttpMethod.Get, "http://savsgbfgnsgndg.com"), CancellationToken.None); } catch (Exception e) { Assert.IsType(e); } // Compare the elapsed time with the given timeout // You can use elapsed.CompareTo(timeout) or simply check if elapsed > timeout, based on your requirement stopwatch.Stop(); var elapsed = stopwatch.Elapsed; var margin = TimeSpan.FromMilliseconds(marginMilliseconds); Assert.True(elapsed >= timeout.Subtract(margin), $"Elapsed time {elapsed} is smaller than expected timeout {timeout} - {marginMilliseconds}ms"); Assert.True(elapsed < timeout.Add(margin), $"Elapsed time {elapsed} is bigger than expected timeout {timeout} + {marginMilliseconds}ms"); return stopwatch; } protected static void AssertTimeoutPrecisely(Stopwatch watcher, TimeSpan expected, TimeSpan? precision = null) { precision ??= TimeSpan.FromMilliseconds(10); TimeSpan elapsed = watcher.Elapsed, margin = elapsed - expected; try { Assert.True(elapsed >= expected, $"Elapsed time {elapsed} is less than expected timeout {expected} with margin {margin}."); } catch (TrueException) { // The elapsed time is approximately 0.998xxx or 2.99xxx, with a 10ms margin of precision accepted. Assert.True(elapsed.Add(precision.Value) >= expected, $"Elapsed time {elapsed} is less than expected timeout {expected} with margin {margin} which module is >= {precision.Value.Milliseconds}ms."); } } protected static TimeoutDelegatingHandler AssertTimeoutDelegatingHandler(HttpMessageInvoker invoker) { Assert.NotNull(invoker); Type me = invoker.GetType(); var field = me.GetField("_handler", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); var handler = field.GetValue(invoker) as HttpMessageHandler; Assert.NotNull(handler); Assert.IsType(handler); return handler as TimeoutDelegatingHandler; } protected static TimeSpan AssertTimeout(HttpMessageInvoker invoker, int expectedSeconds) { var handler = AssertTimeoutDelegatingHandler(invoker); var me = handler.GetType(); var field = me.GetField("_timeout", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); var timeout = (TimeSpan)field.GetValue(handler); Assert.Equal(expectedSeconds, (int)timeout.TotalSeconds); return timeout; } protected static string Url(int port) => $"http://localhost:{port}"; } ================================================ FILE: test/Ocelot.UnitTests/Requester/TimeoutDelegatingHandlerTests.cs ================================================ using Ocelot.Requester; using System.Reflection; namespace Ocelot.UnitTests.Requester; public sealed class TimeoutDelegatingHandlerTests : UnitTest { [Fact] public async Task SendAsync_OnTimeout_ShouldThrowTimeoutException() { // Arrange int ms = 100; using var baseHandler = new SocketsHttpHandler(); using var handler = new TimeoutDelegatingHandler(TimeSpan.FromMilliseconds(ms)); handler.InnerHandler = baseHandler; var type = handler.GetType(); var method = type.GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic); var request = new HttpRequestMessage(HttpMethod.Get, "https://www.nuget.org/"); using var cts = new CancellationTokenSource(); CancellationToken token = cts.Token; // Act var args = new object[] { request, cts.Token }; Task sendAsync() => (Task)method.Invoke(handler, args); async Task sendAsyncAndWaitForTimeout(int delay) { await sendAsync(); await Task.Delay(delay); // wait for Timeout event } // Assert ms += IsCiCd() ? 50 : 0; var ex = await Assert.ThrowsAsync(() => sendAsyncAndWaitForTimeout(ms)); } } ================================================ FILE: test/Ocelot.UnitTests/Responder/AnyError.cs ================================================ using Ocelot.Errors; namespace Ocelot.UnitTests.Responder; internal class AnyError : Error { public AnyError() : base("blahh", OcelotErrorCode.UnknownError, 404) { } public AnyError(OcelotErrorCode errorCode) : base("blah", errorCode, 404) { } } ================================================ FILE: test/Ocelot.UnitTests/Responder/ErrorsToHttpStatusCodeMapperTests.cs ================================================ using Ocelot.Errors; using Ocelot.Responder; namespace Ocelot.UnitTests.Responder; public class ErrorsToHttpStatusCodeMapperTests : UnitTest { private readonly ErrorsToHttpStatusCodeMapper _codeMapper = new(); [Theory] [InlineData(OcelotErrorCode.UnauthenticatedError)] public void Should_return_unauthorized(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Unauthorized); } [Theory] [InlineData(OcelotErrorCode.CannotFindClaimError)] [InlineData(OcelotErrorCode.ClaimValueNotAuthorizedError)] [InlineData(OcelotErrorCode.ScopeNotAuthorizedError)] [InlineData(OcelotErrorCode.UnauthorizedError)] [InlineData(OcelotErrorCode.UserDoesNotHaveClaimError)] public void Should_return_forbidden(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.Forbidden); } [Theory] [InlineData(OcelotErrorCode.RequestTimedOutError)] public void Should_return_service_unavailable(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.ServiceUnavailable); } [Theory] [InlineData(OcelotErrorCode.UnableToCompleteRequestError)] [InlineData(OcelotErrorCode.CouldNotFindLoadBalancerCreator)] [InlineData(OcelotErrorCode.ErrorInvokingLoadBalancerCreator)] public void Should_return_internal_server_error(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.InternalServerError); } [Theory] [InlineData(OcelotErrorCode.ConnectionToDownstreamServiceError)] public void Should_return_bad_gateway_error(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.BadGateway); } [Theory] [InlineData(OcelotErrorCode.CannotAddDataError)] [InlineData(OcelotErrorCode.CannotFindDataError)] [InlineData(OcelotErrorCode.DownstreamHostNullOrEmptyError)] [InlineData(OcelotErrorCode.DownstreamPathNullOrEmptyError)] [InlineData(OcelotErrorCode.DownstreampathTemplateAlreadyUsedError)] [InlineData(OcelotErrorCode.DownstreamPathTemplateContainsSchemeError)] [InlineData(OcelotErrorCode.DownstreamSchemeNullOrEmptyError)] [InlineData(OcelotErrorCode.FileValidationFailedError)] [InlineData(OcelotErrorCode.InstructionNotForClaimsError)] [InlineData(OcelotErrorCode.NoInstructionsError)] [InlineData(OcelotErrorCode.ParsingConfigurationHeaderError)] [InlineData(OcelotErrorCode.RateLimitOptionsError)] [InlineData(OcelotErrorCode.ServicesAreEmptyError)] [InlineData(OcelotErrorCode.ServicesAreNullError)] [InlineData(OcelotErrorCode.UnableToCreateAuthenticationHandlerError)] [InlineData(OcelotErrorCode.UnableToFindDownstreamRouteError)] [InlineData(OcelotErrorCode.UnableToFindLoadBalancerError)] [InlineData(OcelotErrorCode.UnableToFindServiceDiscoveryProviderError)] [InlineData(OcelotErrorCode.UnableToFindQoSProviderError)] [InlineData(OcelotErrorCode.UnknownError)] [InlineData(OcelotErrorCode.UnmappableRequestError)] [InlineData(OcelotErrorCode.UnsupportedAuthenticationProviderError)] public void Should_return_not_found(OcelotErrorCode errorCode) { ShouldMapErrorToStatusCode(errorCode, HttpStatusCode.NotFound); } [Fact] [Trait("Bug", "749")] // https://github.com/ThreeMammals/Ocelot/issues/749 [Trait("PR", "1769")] // https://github.com/ThreeMammals/Ocelot/pull/1769 public void Should_return_request_entity_too_large() { ShouldMapErrorsToStatusCode(new() { OcelotErrorCode.PayloadTooLargeError }, HttpStatusCode.RequestEntityTooLarge); } [Fact] public void AuthenticationErrorsHaveHighestPriority() { var errors = new List { OcelotErrorCode.CannotAddDataError, OcelotErrorCode.CannotFindClaimError, OcelotErrorCode.UnauthenticatedError, OcelotErrorCode.RequestTimedOutError, }; ShouldMapErrorsToStatusCode(errors, HttpStatusCode.Unauthorized); } [Fact] public void AuthorizationErrorsHaveSecondHighestPriority() { var errors = new List { OcelotErrorCode.CannotAddDataError, OcelotErrorCode.CannotFindClaimError, OcelotErrorCode.RequestTimedOutError, }; ShouldMapErrorsToStatusCode(errors, HttpStatusCode.Forbidden); } [Fact] public void ServiceUnavailableErrorsHaveThirdHighestPriority() { var errors = new List { OcelotErrorCode.CannotAddDataError, OcelotErrorCode.RequestTimedOutError, }; ShouldMapErrorsToStatusCode(errors, HttpStatusCode.ServiceUnavailable); } [Fact] public void Check_we_have_considered_all_errors_in_these_tests() { // If this test fails then it's because the number of error codes has changed. // You should make the appropriate changes to the test cases here to ensure // they cover all the error codes, and then modify this assertion. Enum.GetNames().Length.ShouldBe(42, "Looks like the number of error codes has changed. Do you need to modify ErrorsToHttpStatusCodeMapper?"); } private void ShouldMapErrorToStatusCode(OcelotErrorCode errorCode, HttpStatusCode expectedHttpStatusCode) { ShouldMapErrorsToStatusCode(new List { errorCode }, expectedHttpStatusCode); } private void ShouldMapErrorsToStatusCode(List errorCodes, HttpStatusCode expectedHttpStatusCode) { // Arrange var errors = new List(); foreach (var errorCode in errorCodes) { errors.Add(new AnyError(errorCode)); } // Act var result = _codeMapper.Map(errors); // Assert result.ShouldBe((int)expectedHttpStatusCode); } } ================================================ FILE: test/Ocelot.UnitTests/Responder/HttpContextResponderTests.cs ================================================ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Ocelot.Headers; using Ocelot.Middleware; using Ocelot.Responder; namespace Ocelot.UnitTests.Responder; public class HttpContextResponderTests { private readonly HttpContextResponder _responder; public HttpContextResponderTests() { var removeOutputHeaders = new RemoveOutputHeaders(); _responder = new HttpContextResponder(removeOutputHeaders); } [Fact] public async Task Should_remove_transfer_encoding_header() { // Arrange var httpContext = new DefaultHttpContext(); var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { new("Transfer-Encoding", new List {"woop"}), }, "some reason"); // Act await _responder.SetResponseOnHttpContext(httpContext, response); // Assert var header = httpContext.Response.Headers.TransferEncoding; header.ShouldBeEmpty(); } [Fact] public async Task Should_ignore_content_if_null() { // Arrange var httpContext = new DefaultHttpContext(); var response = new DownstreamResponse(null, HttpStatusCode.OK, new List>>(), "some reason"); // Assert await Should.NotThrowAsync(async () => { // Act await _responder.SetResponseOnHttpContext(httpContext, response); }); } [Fact] public async Task Should_have_content_length() { // Arrange var httpContext = new DefaultHttpContext(); var response = new DownstreamResponse(new StringContent("test"), HttpStatusCode.OK, new List>>(), "some reason"); // Act await _responder.SetResponseOnHttpContext(httpContext, response); // Assert var header = httpContext.Response.Headers["Content-Length"]; header.First().ShouldBe("4"); } [Fact] public async Task Should_add_header() { // Arrange var httpContext = new DefaultHttpContext(); var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { new("test", new List {"test"}), }, "some reason"); // Act await _responder.SetResponseOnHttpContext(httpContext, response); // Assert var header = httpContext.Response.Headers["test"]; header.First().ShouldBe("test"); } [Fact] public async Task Should_add_reason_phrase() { // Arrange var httpContext = new DefaultHttpContext(); var response = new DownstreamResponse(new StringContent(string.Empty), HttpStatusCode.OK, new List>> { new("test", new List {"test"}), }, "some reason"); // Act await _responder.SetResponseOnHttpContext(httpContext, response); // Assert httpContext.Response.HttpContext.Features.Get().ReasonPhrase.ShouldBe(response.ReasonPhrase); } [Fact] public void Should_call_without_exception() { // Arrange var httpContext = new DefaultHttpContext(); // Act, Assert _responder.SetErrorResponseOnContext(httpContext, 500); } } ================================================ FILE: test/Ocelot.UnitTests/Responder/ResponderMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Responder; using Ocelot.Responder.Middleware; namespace Ocelot.UnitTests.Responder; public class ResponderMiddlewareTests : UnitTest { private readonly Mock _responder; private readonly Mock _codeMapper; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly ResponderMiddleware _middleware; private readonly RequestDelegate _next; private readonly DefaultHttpContext _httpContext; public ResponderMiddlewareTests() { _httpContext = new DefaultHttpContext(); _responder = new Mock(); _codeMapper = new Mock(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _middleware = new ResponderMiddleware(_next, _responder.Object, _loggerFactory.Object, _codeMapper.Object); } [Fact] public async Task Should_not_return_any_errors() { // Arrange _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage())); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().ShouldBeEmpty(); } [Fact] public async Task Should_return_any_errors() { // Arrange _httpContext.Items.UpsertDownstreamResponse(new DownstreamResponse(new HttpResponseMessage())); _httpContext.Items.SetError(new UnableToFindDownstreamRouteError("/path", "GET")); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().Count.ShouldBe(1); } [Fact] public async Task Should_not_call_responder_when_null_downstream_response() { // Arrange this._responder.Reset(); _httpContext.Items.UpsertDownstreamResponse(null); // Act await _middleware.Invoke(_httpContext); // Assert _httpContext.Items.Errors().ShouldBeEmpty(); _responder.VerifyNoOtherCalls(); } } ================================================ FILE: test/Ocelot.UnitTests/Security/IPSecurityPolicyTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Security.IPSecurity; namespace Ocelot.UnitTests.Security; public sealed class IPSecurityPolicyTests : UnitTest { private readonly DownstreamRouteBuilder _downstreamRouteBuilder; private readonly IPSecurityPolicy _policy; private readonly DefaultHttpContext _context; private readonly SecurityOptionsCreator _securityOptionsCreator; private static readonly FileGlobalConfiguration Empty = new(); public IPSecurityPolicyTests() { _context = new DefaultHttpContext(); _context.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; _downstreamRouteBuilder = new DownstreamRouteBuilder(); _policy = new IPSecurityPolicy(); _securityOptionsCreator = new SecurityOptionsCreator(); } [Fact] public void Should_No_blocked_Ip_and_allowed_Ip() { // Arrange, Act var actual = WhenTheSecurityPolicy(new()); // Assert Assert.False(actual.IsError); } [Fact] public void Should_blockedIp_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.1"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_blockedIp_clientIp_Not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.1"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_allowedIp_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; var options = new FileSecurityOptions("192.168.1.1"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_allowedIp_clientIp_Not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.2")[0]; var options = new FileSecurityOptions("192.168.1.1"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_cidrNotation_allowed24_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.10.5")[0]; var options = new FileSecurityOptions("192.168.1.0/24"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_cidrNotation_allowed24_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.5")[0]; var options = new FileSecurityOptions("192.168.1.0/24"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_cidrNotation_allowed29_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.10")[0]; var options = new FileSecurityOptions("192.168.1.0/29"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_cidrNotation_blocked24_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.1")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0/24"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_cidrNotation_blocked24_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.10.1")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0/24"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_range_allowed_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions("192.168.1.0-192.168.1.10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_range_allowed_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.8")[0]; var options = new FileSecurityOptions("192.168.1.0-192.168.1.10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_range_blocked_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.5")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0-192.168.1.10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_range_blocked_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0-192.168.1.10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_shortRange_allowed_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions("192.168.1.0-10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_shortRange_allowed_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.8")[0]; var options = new FileSecurityOptions("192.168.1.0-10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_shortRange_blocked_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.5")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0-10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_shortRange_blocked_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0-10"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_ipSubnet_allowed_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.10.15")[0]; var options = new FileSecurityOptions("192.168.1.0/255.255.255.0"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_ipSubnet_allowed_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions("192.168.1.0/255.255.255.0"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_ipSubnet_blocked_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.15")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0/255.255.255.0"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_ipSubnet_blocked_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.10.1")[0]; var options = new FileSecurityOptions(blockedIPs: "192.168.1.0/255.255.255.0"); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_exludeAllowedFromBlocked_moreAllowed_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.150")[0]; var options = new FileSecurityOptions("192.168.0.0/255.255.0.0", "192.168.1.100-200", false); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_exludeAllowedFromBlocked_moreAllowed_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.150")[0]; var options = new FileSecurityOptions("192.168.0.0/255.255.0.0", "192.168.1.100-200", true); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] public void Should_exludeAllowedFromBlocked_moreBlocked_clientIp_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.10")[0]; var options = new FileSecurityOptions("192.168.1.10-20", "192.168.1.0/23", false); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.True(actual.IsError); } [Fact] public void Should_exludeAllowedFromBlocked_moreBlocked_clientIp_not_block() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.10")[0]; var options = new FileSecurityOptions("192.168.1.10-20", "192.168.1.0/23", true); // Act var actual = WhenTheSecurityPolicy(options); // Assert Assert.False(actual.IsError); } [Fact] [Trait("Feat", "2170")] public void Should_route_config_overrides_global_config() { // Arrange _context.Connection.RemoteIpAddress = Dns.GetHostAddresses("192.168.1.10")[0]; var globalConfig = new FileGlobalConfiguration { SecurityOptions = new FileSecurityOptions("192.168.1.30-50", "192.168.1.1-100", true), }; var localConfig = new FileSecurityOptions("192.168.1.10", "", false); // Act var actual = WhenTheSecurityPolicy(localConfig, globalConfig); // Assert Assert.False(actual.IsError); } private Response WhenTheSecurityPolicy(FileSecurityOptions options, FileGlobalConfiguration global = null) { // Arrange var securityOptions = _securityOptionsCreator.Create(options, global ?? Empty); _downstreamRouteBuilder.WithSecurityOptions(securityOptions); _context.Items.UpsertDownstreamRoute(_downstreamRouteBuilder.Build()); // Act return _policy.Security(_context.Items.DownstreamRoute(), _context); } } ================================================ FILE: test/Ocelot.UnitTests/Security/SecurityMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Ocelot.Errors; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.Responses; using Ocelot.Security; using Ocelot.Security.Middleware; namespace Ocelot.UnitTests.Security; public sealed class SecurityMiddlewareTests : UnitTest { private readonly List> _securityPolicyList; private readonly Mock _loggerFactory; private readonly Mock _logger; private readonly SecurityMiddleware _middleware; private readonly RequestDelegate _next; private readonly HttpContext _httpContext; public SecurityMiddlewareTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _securityPolicyList = new List> { new(), new(), }; _next = context => Task.CompletedTask; _middleware = new SecurityMiddleware(_next, _loggerFactory.Object, _securityPolicyList.Select(f => f.Object).ToList()); _httpContext.Items.UpsertDownstreamRequest(new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "http://test.com"))); } [Fact] public async Task Should_legal_request() { // Arrange GivenPassingSecurityVerification(); // Act await _middleware.Invoke(_httpContext); // Assert: security passed _httpContext.Items.Errors().Count.ShouldBe(0); } [Fact] public async Task Should_verification_failed_request() { // Arrange GivenNotPassingSecurityVerification(); // Act await _middleware.Invoke(_httpContext); // Assert: security not passed _httpContext.Items.Errors().Count.ShouldBeGreaterThan(0); } private void GivenPassingSecurityVerification() { foreach (var item in _securityPolicyList) { Response response = new OkResponse(); item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response); } } private void GivenNotPassingSecurityVerification() { for (var i = 0; i < _securityPolicyList.Count; i++) { var item = _securityPolicyList[i]; if (i == 0) { Error error = new UnauthenticatedError("Not passing security verification"); Response response = new ErrorResponse(error); item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response); } else { Response response = new OkResponse(); item.Setup(x => x.Security(_httpContext.Items.DownstreamRoute(), _httpContext)).Returns(response); } } } } ================================================ FILE: test/Ocelot.UnitTests/SequentialTests.cs ================================================ namespace Ocelot.UnitTests; /// /// Apply to classes to disable parallelization. /// [CollectionDefinition(nameof(SequentialTests), DisableParallelization = true)] public class SequentialTests { ///// ///// Unstable . ///// //[Collection(nameof(SequentialTests))] //public class KubeTests : Kubernetes.KubeTests //{ } // all tests } ================================================ FILE: test/Ocelot.UnitTests/ServiceDiscovery/ConfigurationServiceProviderTests.cs ================================================ using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.UnitTests.ServiceDiscovery; public class ConfigurationServiceProviderTests : UnitTest { private ConfigurationServiceProvider _serviceProvider; [Fact] public async Task Should_return_services() { // Arrange var hostAndPort = new ServiceHostAndPort("127.0.0.1", 80); var services = new List { new("product", hostAndPort, string.Empty, string.Empty, Array.Empty()), }; _serviceProvider = new ConfigurationServiceProvider(services); // Act var result = await _serviceProvider.GetAsync(); // Assert result[0].HostAndPort.DownstreamHost.ShouldBe(services[0].HostAndPort.DownstreamHost); result[0].HostAndPort.DownstreamPort.ShouldBe(services[0].HostAndPort.DownstreamPort); result[0].Name.ShouldBe(services[0].Name); } } ================================================ FILE: test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs ================================================ using KubeClient; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Kubernetes; using Ocelot.Responses; using Ocelot.ServiceDiscovery; using Ocelot.ServiceDiscovery.Providers; using Ocelot.Values; namespace Ocelot.UnitTests.ServiceDiscovery; public class ServiceDiscoveryProviderFactoryTests : UnitTest { private Response _result; private ServiceDiscoveryProviderFactory _factory; private readonly Mock _loggerFactory; private readonly Mock _logger; private IServiceProvider _provider; private readonly IServiceCollection _collection; public ServiceDiscoveryProviderFactoryTests() { _loggerFactory = new Mock(); _logger = new Mock(); _collection = new ServiceCollection(); _provider = _collection.BuildServiceProvider(true); _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); _loggerFactory.Setup(x => x.CreateLogger()) .Returns(_logger.Object); } [Fact] public void Should_return_no_service_provider() { // Arrange var serviceConfig = new ServiceProviderConfigurationBuilder() .Build(); var route = new DownstreamRouteBuilder().Build(); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert _result.Data.ShouldBeOfType(); } [Fact] public async Task Should_return_list_of_configuration_services() { // Arrange var serviceConfig = new ServiceProviderConfigurationBuilder() .Build(); var downstreamAddresses = new List { new("asdf.com", 80), new("abc.com", 80), }; var route = new DownstreamRouteBuilder().WithDownstreamAddresses(downstreamAddresses).Build(); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert _result.Data.ShouldBeOfType(); // Assert: Then The Following Services Are Returned var result = (ConfigurationServiceProvider)_result.Data; var services = await result.GetAsync(); for (var i = 0; i < services.Count; i++) { var service = services[i]; var downstreamAddress = downstreamAddresses[i]; service.HostAndPort.DownstreamHost.ShouldBe(downstreamAddress.Host); service.HostAndPort.DownstreamPort.ShouldBe(downstreamAddress.Port); } } [Fact] public void Should_return_provider_because_type_matches_reflected_type_from_delegate() { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName("product") .Build(); var serviceConfig = new ServiceProviderConfigurationBuilder() .WithType(nameof(Fake)) .Build(); GivenAFakeDelegate(); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert _result.Data.GetType().Name.ShouldBe("Fake"); } [Fact] public void Should_not_return_provider_because_type_doesnt_match_reflected_type_from_delegate() { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName("product") .Build(); var serviceConfig = new ServiceProviderConfigurationBuilder() .WithType("Wookie") .Build(); GivenAFakeDelegate(); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert _result.IsError.ShouldBeTrue(); _result.Errors.Count.ShouldBe(1); _logInformationMessages.ShouldNotBeNull() .Count.ShouldBe(2); _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Exactly(2)); _logWarningMessages.ShouldNotBeNull() .Count.ShouldBe(1); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); } [Fact] public void Should_return_service_fabric_provider() { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName("product") .Build(); var serviceConfig = new ServiceProviderConfigurationBuilder() .WithType("ServiceFabric") .Build(); GivenAFakeDelegate(); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert _result.Data.ShouldBeOfType(); } [Theory] [Trait("Bug", "1954")] [InlineData("Kube", true)] [InlineData("kube", true)] [InlineData("PollKube", true)] [InlineData("pollkube", true)] [InlineData("unknown", false)] public void Should_return_Kubernetes_provider_with_type_names_from_docs(string typeName, bool success) { // Arrange var route = new DownstreamRouteBuilder() .WithServiceName(TestName()) .Build(); var serviceConfig = new ServiceProviderConfigurationBuilder() .WithType(typeName) .WithPollingInterval(Timeout.Infinite) .Build(); // Arrange: Given Kubernetes Provider var k8sClient = new Mock(); _collection .AddSingleton(KubernetesProviderFactory.Get) .AddSingleton(k8sClient.Object) .AddSingleton(_loggerFactory.Object); _provider = _collection.BuildServiceProvider(true); _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); // Act WhenIGetTheServiceProvider(serviceConfig, route); // Assert if (success) { _result.ShouldBeOfType>(); } else { _result.ShouldBeOfType>(); } } private void GivenAFakeDelegate() { static IServiceDiscoveryProvider fake(IServiceProvider provider, ServiceProviderConfiguration config, DownstreamRoute name) => new Fake(); _collection.AddSingleton((ServiceDiscoveryFinderDelegate)fake); _provider = _collection.BuildServiceProvider(true); _factory = new ServiceDiscoveryProviderFactory(_loggerFactory.Object, _provider); } private class Fake : IServiceDiscoveryProvider { public Task> GetAsync() => null; } private readonly List _logInformationMessages = new(); private readonly List _logWarningMessages = new(); private void WhenIGetTheServiceProvider(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { _logger.Setup(x => x.LogInformation(It.IsAny>())) .Callback>(myFunc => _logInformationMessages.Add(myFunc.Invoke())); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(myFunc => _logWarningMessages.Add(myFunc.Invoke())); _result = _factory.Get(serviceConfig, route); } } ================================================ FILE: test/Ocelot.UnitTests/ServiceDiscovery/ServiceFabricServiceDiscoveryProviderTests.cs ================================================ using Ocelot.ServiceDiscovery.Configuration; using Ocelot.ServiceDiscovery.Providers; namespace Ocelot.UnitTests.ServiceDiscovery; public class ServiceFabricServiceDiscoveryProviderTests : UnitTest { [Fact] public async Task Should_return_service_fabric_naming_service() { // Arrange const string host = "localhost"; const int port = 19081; const string serviceName = "OcelotServiceApplication/OcelotApplicationService"; // Act var config = new ServiceFabricConfiguration(host, port, serviceName); var provider = new ServiceFabricServiceDiscoveryProvider(config); var services = await provider.GetAsync(); // Assert: Then The ServiceFabric Naming Service Is Retured services.Count.ShouldBe(1); services[0].HostAndPort.DownstreamHost.ShouldBe(host); services[0].HostAndPort.DownstreamPort.ShouldBe(port); } } ================================================ FILE: test/Ocelot.UnitTests/ServiceDiscovery/ServiceRegistryTests.cs ================================================ using Ocelot.Values; namespace Ocelot.UnitTests.ServiceDiscovery; public class ServiceRegistryTests : UnitTest { private Service _service; private List _services; private readonly ServiceRegistry _serviceRegistry; private readonly ServiceRepository _serviceRepository; public ServiceRegistryTests() { _serviceRepository = new ServiceRepository(); _serviceRegistry = new ServiceRegistry(_serviceRepository); } [Fact] public void Should_register_service() { // Arrange _service = new Service("product", new ServiceHostAndPort("localhost:5000", 80), string.Empty, string.Empty, Array.Empty()); // Act _serviceRegistry.Register(_service); // Assert: Then The Service Is Registered var serviceNameAndAddress = _serviceRepository.Get(_service.Name); serviceNameAndAddress[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); serviceNameAndAddress[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); serviceNameAndAddress[0].Name.ShouldBe(_service.Name); } [Fact] public void Should_lookup_service() { // Arrange _service = new Service("product", new ServiceHostAndPort("localhost:600", 80), string.Empty, string.Empty, Array.Empty()); _serviceRepository.Set(_service); // Act _services = _serviceRegistry.Lookup("product"); // Assert _services[0].HostAndPort.DownstreamHost.ShouldBe(_service.HostAndPort.DownstreamHost); _services[0].HostAndPort.DownstreamPort.ShouldBe(_service.HostAndPort.DownstreamPort); _services[0].Name.ShouldBe(_service.Name); } } public interface IServiceRegistry { void Register(Service serviceNameAndAddress); List Lookup(string name); } public class ServiceRegistry : IServiceRegistry { private readonly IServiceRepository _repository; public ServiceRegistry(IServiceRepository repository) => _repository = repository; public void Register(Service serviceNameAndAddress) => _repository.Set(serviceNameAndAddress); public List Lookup(string name) => _repository.Get(name); } public interface IServiceRepository { List Get(string serviceName); void Set(Service serviceNameAndAddress); } public class ServiceRepository : IServiceRepository { private readonly Dictionary> _registeredServices; public ServiceRepository() => _registeredServices = new Dictionary>(); public List Get(string serviceName) => _registeredServices[serviceName]; public void Set(Service serviceNameAndAddress) { if (_registeredServices.TryGetValue(serviceNameAndAddress.Name, out var services)) { services.Add(serviceNameAndAddress); _registeredServices[serviceNameAndAddress.Name] = services; } else { _registeredServices[serviceNameAndAddress.Name] = new List { serviceNameAndAddress }; } } } ================================================ FILE: test/Ocelot.UnitTests/TestRetry.cs ================================================ using Ocelot.Infrastructure.DesignPatterns; using Ocelot.Logging; namespace Ocelot.UnitTests; public static class TestRetry { public static TResult NoWait( Func operation, Predicate predicate = null, int retryTimes = Retry.DefaultRetryTimes, IOcelotLogger logger = null) => Retry.Operation(operation, predicate, retryTimes, 0, logger); public static Task NoWaitAsync( Func> operation, Predicate predicate = null, int retryTimes = Retry.DefaultRetryTimes, IOcelotLogger logger = null) => Retry.OperationAsync(operation, predicate, retryTimes, 0, logger); } ================================================ FILE: test/Ocelot.UnitTests/UnitTest.cs ================================================ using Ocelot.Configuration.File; using Ocelot.Infrastructure.Extensions; using System.Runtime.CompilerServices; namespace Ocelot.UnitTests; public class UnitTest : Unit { //protected static FileRouteBox Box(FileRoute route) => new(route); } ================================================ FILE: test/Ocelot.UnitTests/UnitTests.runsettings ================================================ opencover false true ================================================ FILE: test/Ocelot.UnitTests/Usings.cs ================================================ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces global using Moq; global using Ocelot; global using Ocelot.Testing; global using Ocelot.Testing.Boxing; global using Shouldly; global using System.Net; global using Xunit; using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("Usage", "xUnit1004:Test methods should not be skipped", Justification = "Reviewed.")] internal class Usings { } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/ClientWebSocketConnectorTests.cs ================================================ using Ocelot.WebSockets; using System.Net.Security; using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; namespace Ocelot.UnitTests.WebSockets; public sealed class ClientWebSocketConnectorTests : UnitTest, IDisposable { private readonly ClientWebSocket _injectee; // no mocking private readonly ClientWebSocketConnector _connector; public ClientWebSocketConnectorTests() { _injectee = new(); // no mocking _connector = new(_injectee); } public void Dispose() => _injectee.Dispose(); [Fact] public void ToWebSocket_ReturnedConcrete() { // Arrange, Act var actual = _connector.ToWebSocket(); // Assert Assert.NotNull(actual); Assert.IsType(actual); Assert.Equal(_injectee, actual); } [Fact] public void Options_ReturnedProxy() { // Arrange static bool RemoteCertificateValidation(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true; _injectee.Options.RemoteCertificateValidationCallback = RemoteCertificateValidation; // Act var actual = _connector.Options; // Assert Assert.NotNull(actual); Assert.IsType(actual); Assert.Equal(RemoteCertificateValidation, actual.RemoteCertificateValidationCallback); } [Fact] public async Task ConnectAsync_Proxied() { // Arrange, Act var url = new UriBuilder(Uri.UriSchemeWss, "echo.websocket.org"); await _connector.ConnectAsync(url.Uri, CancellationToken.None); // Assert Assert.Equal(WebSocketState.Open, _injectee.State); await _injectee.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, nameof(ConnectAsync_Proxied), CancellationToken.None); Assert.Equal(WebSocketState.CloseSent, _injectee.State); await _injectee.CloseAsync(WebSocketCloseStatus.NormalClosure, nameof(ConnectAsync_Proxied), CancellationToken.None); Assert.Equal(WebSocketState.Closed, _injectee.State); } } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/ClientWebSocketOptionsProxyTests.cs ================================================ using Ocelot.WebSockets; using System.Net.Security; using System.Net.WebSockets; using System.Reflection; using System.Security.Cryptography.X509Certificates; namespace Ocelot.UnitTests.WebSockets; public sealed class ClientWebSocketOptionsProxyTests : UnitTest, IDisposable { private readonly ClientWebSocket _socket; private readonly ClientWebSocketOptionsProxy _proxy; public ClientWebSocketOptionsProxyTests() { _socket = new ClientWebSocket(); _proxy = new ClientWebSocketOptionsProxy(_socket.Options); } public void Dispose() { _socket.Dispose(); } [Fact] public void HttpVersion_Proxied() { // Arrange var expected = new Version(1, 22, 333, 4444); _socket.Options.HttpVersion = expected; // Act var actual = _proxy.HttpVersion; // Assert Assert.NotNull(actual); Assert.Equal(expected, actual); } [Fact] public void HttpVersionPolicy_Proxied() { // Arrange var expected = HttpVersionPolicy.RequestVersionOrHigher; _socket.Options.HttpVersionPolicy = expected; // Act var actual = _proxy.HttpVersionPolicy; // Assert Assert.Equal(expected, actual); } [Fact] public void UseDefaultCredentials_Proxied() { // Arrange var expected = true; _socket.Options.UseDefaultCredentials = expected; // Act var actual = _proxy.UseDefaultCredentials; // Assert Assert.Equal(expected, actual); } [Fact] public void Credentials_Proxied() { // Arrange var expected = new NetworkCredential("test", nameof(Credentials_Proxied)); var cr = new Mock(); cr.Setup(x => x.GetCredential(It.IsAny(), It.IsAny())) .Returns(expected); _socket.Options.Credentials = cr.Object; // Act var actual = _proxy.Credentials; var actualCredential = actual.GetCredential(new("https://ocelot.net"), string.Empty); // Assert Assert.NotNull(actual); Assert.NotNull(actualCredential); Assert.Equal(expected, actualCredential); Assert.Equal("test", actualCredential.UserName); Assert.Equal(nameof(Credentials_Proxied), actualCredential.Password); } [Fact] public void Proxy_Proxied() { // Arrange var expected = new Uri("https://ocelot.net"); var pr = new Mock(); pr.Setup(x => x.GetProxy(It.IsAny())) .Returns(new Uri("https://ocelot.net")); _socket.Options.Proxy = pr.Object; // Act var actual = _proxy.Proxy; var actualProxy = actual.GetProxy(new Uri("https://ocelot.blog")); // Assert Assert.NotNull(actual); Assert.NotNull(actualProxy); Assert.Equal(expected, actualProxy); Assert.Equal(expected.Host, actualProxy.Host); } [Fact] public void ClientCertificates_Proxied() { // Arrange #pragma warning disable SYSLIB0026 // Type or member is obsolete var expected = new X509CertificateCollection { new() }; _socket.Options.ClientCertificates = expected; // Act var actual = _proxy.ClientCertificates; // Assert Assert.NotNull(actual); Assert.Equal(expected, actual); Assert.Single(actual); } [Fact] public void RemoteCertificateValidationCallback_Proxied() { static bool FakeCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) => true; // Arrange RemoteCertificateValidationCallback expected = FakeCallback; _socket.Options.RemoteCertificateValidationCallback = expected; // Act var actual = _proxy.RemoteCertificateValidationCallback; var actualValue = actual?.Invoke(new(), new(), new(), new()); // Assert Assert.NotNull(actual); Assert.Equal(expected, actual); Assert.True(actualValue); } [Fact] public void Cookies_Proxied() { // Arrange var expected = new CookieContainer(); var host = new Uri("https://ocelot.net"); var cookie = new Cookie("test", nameof(Cookies_Proxied)); expected.Add(host, cookie); _socket.Options.Cookies = expected; // Act var actual = _proxy.Cookies; // Assert Assert.NotNull(actual); Assert.Equal(expected, actual); Assert.Equal(1, actual.Count); } [Fact] public void KeepAliveInterval_Proxied() { // Arrange var expected = TimeSpan.FromMilliseconds(1234); _socket.Options.KeepAliveInterval = expected; // Act var actual = _proxy.KeepAliveInterval; // Assert Assert.Equal(expected, actual); Assert.Equal(1234, (int)actual.TotalMilliseconds); } [Fact] public void DangerousDeflateOptions_Proxied() { // Arrange var expected = new WebSocketDeflateOptions { ClientMaxWindowBits = 12 }; _socket.Options.DangerousDeflateOptions = expected; // Act var actual = _proxy.DangerousDeflateOptions; // Assert Assert.Equal(expected, actual); Assert.Equal(12, actual.ClientMaxWindowBits); } [Fact] public void CollectHttpResponseDetails_Proxied() { // Arrange var expected = true; _socket.Options.CollectHttpResponseDetails = expected; // Act var actual = _proxy.CollectHttpResponseDetails; // Assert Assert.Equal(expected, actual); } private static readonly Type Me = typeof(ClientWebSocketOptions); [Fact] public void AddSubProtocol_Proxied() { // Arrange var expected = nameof(AddSubProtocol_Proxied); // Act _proxy.AddSubProtocol(expected); // Assert var prop = Me.GetProperty("RequestedSubProtocols", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(prop); var actual = prop.GetValue(_socket.Options) as List; Assert.NotNull(actual); Assert.Contains(expected, actual); } [Fact] public void SetBuffer_Proxied() { // Arrange int expected = 1234; // Act _proxy.SetBuffer(expected, 1); // Assert var field = Me.GetField("_receiveBufferSize", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); int actual = (int)field.GetValue(_socket.Options); Assert.Equal(expected, actual); } [Fact] public void SetBuffer_ArraySegment_Proxied() { // Arrange int expected = 1234; var buffer = new ArraySegment(new byte[] { 1, 2, 3, 4 }); // Act _proxy.SetBuffer(expected, 1, buffer); // Assert var field = Me.GetField("_receiveBufferSize", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); int actual = (int)field.GetValue(_socket.Options); Assert.Equal(expected, actual); field = Me.GetField("_buffer", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(field); ArraySegment segment = (ArraySegment)field.GetValue(_socket.Options); Assert.Equal(buffer, segment); } [Fact] public void SetRequestHeader_Proxied() { // Arrange var expected = nameof(SetRequestHeader_Proxied); // Act _proxy.SetRequestHeader("test", expected); // Assert var prop = Me.GetProperty("RequestHeaders", BindingFlags.NonPublic | BindingFlags.Instance); Assert.NotNull(prop); var actual = prop.GetValue(_socket.Options) as WebHeaderCollection; Assert.NotNull(actual); Assert.Single(actual); Assert.Equal(nameof(SetRequestHeader_Proxied), actual.Get("test")); } } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/ClientWebSocketProxyTests.cs ================================================ using Ocelot.WebSockets; using System.Net.WebSockets; namespace Ocelot.UnitTests.WebSockets; public sealed class ClientWebSocketProxyTests : UnitTest, IDisposable { private readonly ClientWebSocketProxy _proxy; private readonly Mock _socket; private readonly Mock _connector; public ClientWebSocketProxyTests() { _socket = new Mock(); _connector = new Mock(); _proxy = new(_socket.Object, _connector.Object); } public void Dispose() => _proxy.Dispose(); [Fact] public void ToWebSocket_NoCasting() { // Arrange, Act var actual = _proxy.ToWebSocket(); // Assert Assert.NotNull(actual); Assert.Equal(_socket.Object, actual); } [Fact] public void Options_Proxied() { // Arrange var options = new Mock(); _connector.SetupGet(x => x.Options) .Returns(options.Object).Verifiable(); // Act var actual = _proxy.Options; // Assert Assert.NotNull(actual); _connector.VerifyGet(x => x.Options, Times.Once()); } [Fact] public async Task ConnectAsync_Proxied() { // Arrange var options = new Mock(); _connector.Setup(x => x.ConnectAsync(It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask).Verifiable(); // Act await _proxy.ConnectAsync(new("https://ocelot.net"), CancellationToken.None); // Assert _connector.Verify( x => x.ConnectAsync(It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public void CloseStatus_Proxied() { // Arrange _socket.SetupGet(x => x.CloseStatus) .Returns(WebSocketCloseStatus.Empty).Verifiable(); // Act var actual = _proxy.CloseStatus; // Assert Assert.NotNull(actual); _socket.VerifyGet(x => x.CloseStatus, Times.Once()); } [Fact] public void CloseStatusDescription_Proxied() { // Arrange _socket.SetupGet(x => x.CloseStatusDescription) .Returns(string.Empty).Verifiable(); // Act var actual = _proxy.CloseStatusDescription; // Assert Assert.NotNull(actual); _socket.VerifyGet(x => x.CloseStatusDescription, Times.Once()); } [Fact] public void State_Proxied() { // Arrange _socket.SetupGet(x => x.State) .Returns(WebSocketState.None).Verifiable(); // Act var actual = _proxy.State; // Assert Assert.Equal(WebSocketState.None, actual); _socket.VerifyGet(x => x.State, Times.Once()); } [Fact] public void SubProtocol_Proxied() { // Arrange _socket.SetupGet(x => x.SubProtocol) .Returns(Uri.UriSchemeWss).Verifiable(); // Act var actual = _proxy.SubProtocol; // Assert Assert.Equal(Uri.UriSchemeWss, actual); _socket.VerifyGet(x => x.SubProtocol, Times.Once()); } [Fact] public void Abort_Proxied() { // Arrange _socket.Setup(x => x.Abort()).Verifiable(); // Act _proxy.Abort(); // Assert _socket.Verify(x => x.Abort(), Times.Once()); } [Fact] public async Task CloseAsync_Proxied() { // Arrange _socket.Setup(x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Verifiable(); // Act await _proxy.CloseAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None); // Assert _socket.Verify( x => x.CloseAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task CloseOutputAsync_Proxied() { // Arrange _socket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Verifiable(); // Act await _proxy.CloseOutputAsync(WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None); // Assert _socket.Verify( x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } [Fact] public async Task ReceiveAsync_Proxied() { // Arrange var expected = new WebSocketReceiveResult(123, WebSocketMessageType.Binary, true); _socket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(expected).Verifiable(); // Act var actual = await _proxy.ReceiveAsync(ArraySegment.Empty, CancellationToken.None); // Assert Assert.Equal(expected, actual); _socket.Verify( x => x.ReceiveAsync(It.IsAny>(), It.IsAny()), Times.Once()); } [Fact] public async Task SendAsync_Proxied() { // Arrange var expected = new WebSocketReceiveResult(123, WebSocketMessageType.Binary, true); _socket.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask).Verifiable(); // Act await _proxy.SendAsync(ArraySegment.Empty, WebSocketMessageType.Binary, true, CancellationToken.None); // Assert _socket.Verify( x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); } } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/MockWebSocket.cs ================================================ // Copyright © Kubernetes C# Client // Repository: https://github.com/kubernetes-client/csharp // Class: https://github.com/kubernetes-client/csharp/blob/master/tests/KubernetesClient.Tests/Mock/MockWebSocket.cs using Nito.AsyncEx; using System.Collections.Concurrent; using System.Net.WebSockets; namespace Ocelot.UnitTests.WebSockets; internal class MockWebSocket : WebSocket { private WebSocketCloseStatus? closeStatus; private string closeStatusDescription; private WebSocketState state; private readonly string subProtocol; private readonly ConcurrentQueue receiveBuffers = new(); private readonly AsyncAutoResetEvent receiveEvent = new(false); private bool disposedValue; public MockWebSocket(string subProtocol = null) => this.subProtocol = subProtocol; public void SetState(WebSocketState state) => this.state = state; public EventHandler MessageSent { get; set; } public Task InvokeReceiveAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage) { receiveBuffers.Enqueue(new MessageData() { Buffer = buffer, MessageType = messageType, EndOfMessage = endOfMessage, }); receiveEvent.Set(); return Task.CompletedTask; } public override WebSocketCloseStatus? CloseStatus => closeStatus; public override string CloseStatusDescription => closeStatusDescription; public override WebSocketState State => state; public override string SubProtocol => subProtocol; public override void Abort() => throw new NotImplementedException(); public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { this.closeStatus = closeStatus; closeStatusDescription = statusDescription; receiveBuffers.Enqueue(new MessageData() { Buffer = new ArraySegment(Array.Empty()), EndOfMessage = true, MessageType = WebSocketMessageType.Close, }); receiveEvent.Set(); return Task.CompletedTask; } public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => throw new NotImplementedException(); public override async Task ReceiveAsync( ArraySegment buffer, CancellationToken cancellationToken) { if (receiveBuffers.IsEmpty) { await receiveEvent.WaitAsync(cancellationToken).ConfigureAwait(false); } var bytesReceived = 0; var endOfMessage = true; var messageType = WebSocketMessageType.Close; if (receiveBuffers.TryPeek(out MessageData received)) { messageType = received.MessageType; if (received.Buffer.Count <= buffer.Count) { receiveBuffers.TryDequeue(out received); received.Buffer.CopyTo(buffer); bytesReceived = received.Buffer.Count; endOfMessage = received.EndOfMessage; } else { received.Buffer.Slice(0, buffer.Count).CopyTo(buffer); bytesReceived = buffer.Count; endOfMessage = false; received.Buffer = received.Buffer.Slice(buffer.Count); } } return new WebSocketReceiveResult(bytesReceived, messageType, endOfMessage); } public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { MessageSent?.Invoke( this, new MessageDataEventArgs() { Data = new MessageData() { Buffer = buffer, MessageType = messageType, EndOfMessage = endOfMessage, }, }); return Task.CompletedTask; } public class MessageData { public ArraySegment Buffer { get; set; } public WebSocketMessageType MessageType { get; set; } public bool EndOfMessage { get; set; } } public class MessageDataEventArgs : EventArgs { public MessageData Data { get; set; } } protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { receiveBuffers.Clear(); receiveEvent.Set(); } disposedValue = true; } } // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources // ~MockWebSocket() // { // // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method // Dispose(disposing: false); // } public override void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(true); GC.SuppressFinalize(this); } } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/WebSocketsFactoryTests.cs ================================================ using Ocelot.WebSockets; namespace Ocelot.UnitTests.WebSockets; public class WebSocketsFactoryTests { [Fact] public void CreateClient_Created() { // Arrange WebSocketsFactory factory = new(); // Act var actual = factory.CreateClient(); // Assert Assert.NotNull(actual); Assert.IsType(actual); } } ================================================ FILE: test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs ================================================ using Microsoft.AspNetCore.Http; using Moq.Protected; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Middleware; using Ocelot.Request.Middleware; using Ocelot.WebSockets; using System.Linq.Expressions; using System.Net.Security; using System.Net.WebSockets; using System.Reflection; namespace Ocelot.UnitTests.WebSockets; public class WebSocketsProxyMiddlewareTests : UnitTest { private WebSocketsProxyMiddleware _middleware; private readonly Mock _loggerFactory; private readonly Mock _next; private readonly Mock _factory; private readonly Mock _context; private readonly Mock _logger; private readonly Mock _client; public WebSocketsProxyMiddlewareTests() { _loggerFactory = new Mock(); _next = new Mock(); _factory = new Mock(); _context = new Mock(); _context.SetupGet(x => x.WebSockets.IsWebSocketRequest).Returns(true); _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()) .Returns(_logger.Object); _middleware = new WebSocketsProxyMiddleware(_loggerFactory.Object, _next.Object, _factory.Object); _client = new Mock(); _factory.Setup(x => x.CreateClient()).Returns(_client.Object); } [Fact] public async Task Proxy_NotIsWebSocketRequest_ThrownException() { // Arrange List messages = new(); GivenNonWebsocketScheme(Uri.UriSchemeHttps, messages); _context.SetupGet(x => x.WebSockets.IsWebSocketRequest).Returns(false); // Act Task action() => _middleware.Invoke(_context.Object); // Assert var ex = await Assert.ThrowsAsync(action); Assert.NotNull(ex); } [Fact] public async Task Proxy_ThereAreWebSocketRequestedProtocols_AddedSubProtocols() { // Arrange List messages = new(); GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndSetupProtocolsAndHeaders( new() { Uri.UriSchemeHttps, Uri.UriSchemeWs, Uri.UriSchemeWss }, null); AndDoNotConnectReally(null); var options = new Mock(); _client.SetupGet(x => x.Options) .Returns(options.Object).Verifiable(); var actualProtos = new List(); options.Setup(x => x.AddSubProtocol(It.IsAny())) .Callback(actualProtos.Add).Verifiable(); // Act await _middleware.Invoke(_context.Object); // Assert _client.VerifyGet(x => x.Options, Times.Exactly(3)); options.Verify(x => x.AddSubProtocol(It.IsAny()), Times.Exactly(3)); Assert.Equal(3, actualProtos.Count); } [Fact] public async Task Proxy_ThereAreHeaders_SetRequestHeaders() { // Arrange List messages = new(); HeaderDictionary headers = new() { { "TestMe", nameof(Proxy_ThereAreHeaders_SetRequestHeaders) }, }; GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndSetupProtocolsAndHeaders(null, headers); AndDoNotConnectReally(null); var options = new Mock(); _client.SetupGet(x => x.Options).Returns(options.Object).Verifiable(); var actual = new Dictionary(); options.Setup(x => x.SetRequestHeader(It.IsAny(), It.IsAny())) .Callback(actual.Add).Verifiable(); // Act await _middleware.Invoke(_context.Object); // Assert _client.VerifyGet(x => x.Options, Times.Exactly(1)); options.Verify(x => x.SetRequestHeader(It.IsAny(), It.IsAny()), Times.Exactly(1)); Assert.Single(actual); Assert.True(actual.ContainsKey("TestMe")); Assert.Equal(nameof(Proxy_ThereAreHeaders_SetRequestHeaders), actual["TestMe"]); } [Fact] public async Task Proxy_ThereAreHeaders_ThrownExceptionButCaughtIt() { // Arrange List messages = new(); HeaderDictionary headers = new() { { "TestMe", nameof(Proxy_ThereAreHeaders_ThrownExceptionButCaughtIt) }, }; GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndSetupProtocolsAndHeaders(null, headers); AndDoNotConnectReally(null); var options = new Mock(); _client.SetupGet(x => x.Options).Returns(options.Object).Verifiable(); var actual = new Dictionary(); options.Setup(x => x.SetRequestHeader(It.IsAny(), It.IsAny())) .Throws(new ArgumentException()); // !!! // Act await _middleware.Invoke(_context.Object); // Assert _client.VerifyGet(x => x.Options, Times.Exactly(1)); options.Verify(x => x.SetRequestHeader(It.IsAny(), It.IsAny()), Times.Exactly(1)); Assert.Empty(actual); } [Fact] [Trait("Bug", "1375 1237 925 920")] [Trait("PR", "1377")] // https://github.com/ThreeMammals/Ocelot/pull/1377 public async Task ShouldIgnoreAllSslWarningsWhenDangerousAcceptAnyServerCertificateValidatorIsTrue() { // Arrange List actual = new(); GivenPropertyDangerousAcceptAnyServerCertificateValidator(true, actual); AndDoNotSetupProtocolsAndHeaders(); AndDoNotConnectReally(null); // Act await _middleware.Invoke(_context.Object); // Assert ThenIgnoredAllSslWarnings(actual); } private void GivenPropertyDangerousAcceptAnyServerCertificateValidator(bool enabled, List messages) { var request = new HttpRequestMessage(HttpMethod.Get, new UriBuilder(Uri.UriSchemeWs, "localhost", PortFinder.GetRandomPort()).Uri); var downstream = new DownstreamRequest(request); var route = new DownstreamRouteBuilder() .WithDangerousAcceptAnyServerCertificateValidator(enabled) .Build(); _context.SetupGet(x => x.Items).Returns(new Dictionary { { nameof(DownstreamRequest), downstream }, { nameof(DownstreamRoute), route }, }); _client.SetupSet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny()) .Callback(messages.Add); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(y => messages.Add(y.Invoke())); } private void AndDoNotSetupProtocolsAndHeaders() => AndSetupProtocolsAndHeaders(null, null); private void AndSetupProtocolsAndHeaders(List protos = null, HeaderDictionary headers = null) { _context.SetupGet(x => x.WebSockets.WebSocketRequestedProtocols).Returns(protos ?? new()); _context.SetupGet(x => x.Request.Headers).Returns(headers ?? new()); } private Mock DoNotConnectReally(Action callbackConnectAsync, out Mock server) { Action doNothing = (u, t) => { }; _client.Setup(x => x.ConnectAsync(It.IsAny(), It.IsAny())) .Callback(callbackConnectAsync ?? doNothing); var clientSocket = new Mock(); var serverSocket = new Mock(); _client.Setup(x => x.ToWebSocket()).Returns(clientSocket.Object); _context.Setup(x => x.WebSockets.AcceptWebSocketAsync(It.IsAny())).ReturnsAsync(serverSocket.Object); server = serverSocket; return clientSocket; } private void AndDoNotConnectReally(Action callbackConnectAsync) { var clientSocket = DoNotConnectReally(callbackConnectAsync, out var serverSocket); var happyEnd = new WebSocketReceiveResult(1, WebSocketMessageType.Close, true); clientSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(happyEnd); serverSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(happyEnd); clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())); serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())); clientSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty); serverSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty); } private void ThenIgnoredAllSslWarnings(List actual) { var route = _context.Object.Items.DownstreamRoute(); var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeTrue(); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.Last() as string; warning.ShouldNotBeNullOrEmpty(); var expectedWarning = string.Format(WebSocketsProxyMiddleware.IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate); warning.ShouldBe(expectedWarning); _client.VerifySet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny(), Times.Once()); var callback = actual.First() as RemoteCertificateValidationCallback; callback.ShouldNotBeNull(); var validation = callback.Invoke(null, null, null, SslPolicyErrors.None); validation.ShouldBeTrue(); } [Theory] [Trait("Bug", "1509 1683")] [Trait("PR", "1689")] // https://github.com/ThreeMammals/Ocelot/pull/1689 [InlineData("http", "ws")] [InlineData("https", "wss")] [InlineData("ftp", "ftp")] public async Task ShouldReplaceNonWsSchemes(string scheme, string expectedScheme) { // Arrange List actual = new(); GivenNonWebsocketScheme(scheme, actual); AndDoNotSetupProtocolsAndHeaders(); AndDoNotConnectReally((uri, token) => actual.Add(uri)); // Act await _middleware.Invoke(_context.Object); // Assert ThenNonWsSchemesAreReplaced(scheme, expectedScheme, actual); } private void GivenNonWebsocketScheme(string scheme, List messages) { var requestMessage = new HttpRequestMessage(HttpMethod.Get, $"{scheme}://localhost:12345"); var request = new DownstreamRequest(requestMessage); var route = new DownstreamRouteBuilder().Build(); var items = new Dictionary { { nameof(DownstreamRequest), request }, { nameof(DownstreamRoute), route }, }; _context.SetupGet(x => x.Items).Returns(items); _logger.Setup(x => x.LogWarning(It.IsAny>())) .Callback>(myFunc => messages.Add(myFunc.Invoke())); } private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, List actual) { var route = _context.Object.Items.DownstreamRoute(); var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeFalse(); _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.First() as string; warning.ShouldNotBeNullOrEmpty(); warning.ShouldContain($"'{scheme}'"); var expectedWarning = string.Format(WebSocketsProxyMiddleware.InvalidSchemeWarningFormat, scheme, request.ToUri().Replace(expectedScheme, scheme)); warning.ShouldBe(expectedWarning); request.Scheme.ShouldBe(expectedScheme); ((Uri)actual.Last()).Scheme.ShouldBe(expectedScheme); } private static WebSocketCloseStatus[] AndBothSocketsGenerateExceptionWhenReceiveAsync(Mock clientSocket, Mock serverSocket, Exception error, Func closing) { var actual = new WebSocketCloseStatus[2]; var cresult = clientSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ThrowsAsync(error); clientSocket.SetupGet(x => x.State).Returns(WebSocketState.Open); clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(closing) .Callback((s, d, t) => actual[0] = s); var sresult = serverSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ThrowsAsync(error); serverSocket.SetupGet(x => x.State).Returns(WebSocketState.Open); serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(closing) .Callback((s, d, t) => actual[1] = s); return actual; } private static void ThenBothSocketsClosedOutputTimes(Mock clientSocket, Mock serverSocket, Times howMany) { clientSocket.Verify( x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()), howMany); serverSocket.Verify( x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()), howMany); } [Fact] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 public async Task PumpAsync_OperationCanceledException_ClosedDestinationSocket() { // Arrange bool closed = false; Task Closing() { closed = true; return Task.CompletedTask; } var messages = new List(); GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndDoNotSetupProtocolsAndHeaders(); var clientSocket = DoNotConnectReally(null, out var serverSocket); var error = new OperationCanceledException(); var actual = AndBothSocketsGenerateExceptionWhenReceiveAsync(clientSocket, serverSocket, error, Closing); // Act await _middleware.Invoke(_context.Object); // Assert ThenBothSocketsClosedOutputTimes(clientSocket, serverSocket, Times.Once()); Assert.True(closed); Assert.All(actual, s => Assert.Equal(WebSocketCloseStatus.EndpointUnavailable, s)); } [Fact] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 public async Task PumpAsync_WebSocketException_ClosedDestinationSocket() { // Arrange bool closed = false; Task Closing() { closed = true; return Task.CompletedTask; } var messages = new List(); GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndDoNotSetupProtocolsAndHeaders(); var clientSocket = DoNotConnectReally(null, out var serverSocket); var error = new WebSocketException(WebSocketError.ConnectionClosedPrematurely); var actual = AndBothSocketsGenerateExceptionWhenReceiveAsync(clientSocket, serverSocket, error, Closing); // Act await _middleware.Invoke(_context.Object); // Assert ThenBothSocketsClosedOutputTimes(clientSocket, serverSocket, Times.Once()); Assert.True(closed); Assert.All(actual, s => Assert.Equal(WebSocketCloseStatus.EndpointUnavailable, s)); } [Fact] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 public async Task PumpAsync_IsOpen_SentToDestination() { // Arrange var messages = new List(); GivenPropertyDangerousAcceptAnyServerCertificateValidator(false, messages); AndDoNotSetupProtocolsAndHeaders(); var clientSocket = DoNotConnectReally(null, out var serverSocket); int clientCount = 0, serverCount = 0; var open = new WebSocketReceiveResult(1, WebSocketMessageType.Binary, true); var close = new WebSocketReceiveResult(1, WebSocketMessageType.Close, true); clientSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(() => clientCount++ < 1 ? open : close); serverSocket.Setup(x => x.ReceiveAsync(It.IsAny>(), It.IsAny())) .ReturnsAsync(() => serverCount++ < 1 ? open : close); clientSocket.SetupGet(x => x.State).Returns(() => clientCount < 1 ? WebSocketState.Open : WebSocketState.Closed); serverSocket.SetupGet(x => x.State).Returns(() => serverCount < 1 ? WebSocketState.Open : WebSocketState.Closed); clientSocket.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())); serverSocket.Setup(x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny())); clientSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())); serverSocket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())); clientSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty); serverSocket.SetupGet(x => x.CloseStatus).Returns(WebSocketCloseStatus.Empty); clientSocket.SetupGet(x => x.CloseStatusDescription).Returns("closed"); serverSocket.SetupGet(x => x.CloseStatusDescription).Returns("closed"); // Act await _middleware.Invoke(_context.Object); // Assert Expression> closeOutputAsync = x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()); Expression> sendAsync = x => x.SendAsync(It.IsAny>(), It.IsAny(), It.IsAny(), It.IsAny()); clientSocket.Verify(closeOutputAsync, Times.Never()); serverSocket.Verify(closeOutputAsync, Times.Once()); clientSocket.Verify(sendAsync, Times.Never()); serverSocket.Verify(sendAsync, Times.Once()); Assert.Equal(2, clientCount); Assert.Equal(2, serverCount); } private static readonly Type Me = typeof(WebSocketsProxyMiddleware); private Mock MockMiddleware() { static Task Next(HttpContext context) => Task.Delay(10); RequestDelegate requestDelegate = Next; var mock = new Mock(requestDelegate, _loggerFactory.Object, _factory.Object) { CallBase = true }; mock.Protected() .Setup("TryCloseOutputAsync", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) .Returns(Task.CompletedTask).Verifiable(); _middleware = mock.Object; return mock; } [Fact] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 public async Task TryCloseOutputAsync_NoState_NoClosing() { // Arrange //var mock = MockMiddleware(); var socket = new Mock(); socket.SetupGet(x => x.State) .Returns(WebSocketState.None); socket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Task.CompletedTask).Verifiable(); // Act var method = Me.GetMethod("TryCloseOutputAsync", BindingFlags.Instance | BindingFlags.NonPublic); var actual = (Task)method.Invoke(_middleware, [ socket.Object, WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None ]); await actual; // Assert Assert.True(actual.IsCompleted); Assert.Equal(Task.CompletedTask, actual); socket.Verify( x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } [Theory] [Trait("Bug", "930")] [Trait("PR", "2091")] // https://github.com/ThreeMammals/Ocelot/pull/2091 [InlineData(WebSocketState.Open)] [InlineData(WebSocketState.CloseReceived)] public async Task TryCloseOutputAsync_MatchingState_HappyPath(WebSocketState state) { bool closed = false; Task Closing() { closed = true; return Task.CompletedTask; } // Arrange var socket = new Mock(); socket.SetupGet(x => x.State) .Returns(state); socket.Setup(x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny())) .Returns(Closing).Verifiable(); // Act var method = Me.GetMethod("TryCloseOutputAsync", BindingFlags.Instance | BindingFlags.NonPublic); var actual = (Task)method.Invoke(_middleware, [socket.Object, WebSocketCloseStatus.Empty, string.Empty, CancellationToken.None]); await actual; // Assert Assert.True(actual.IsCompleted); Assert.Equal(Task.CompletedTask, actual); socket.Verify( x => x.CloseOutputAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); Assert.True(closed); } } ================================================ FILE: test/Ocelot.UnitTests/appsettings.json ================================================ { "Logging": { "LogLevel": { "Default": "Trace", "Microsoft.AspNetCore": "Trace" } }, "spring": { "application": { "name": "ocelot" } }, "eureka": { "client": { "serviceUrl": "http://localhost:8761/eureka/", "shouldRegisterWithEureka": true, "shouldFetchRegistry": true, "port": 5000, "hostName": "localhost" } }, "BaseUrl": "http://foo-bar.co.uk", "TestConfig": "foo", "TestConfigNested":{ "Child": "foo" } } ================================================ FILE: test/Ocelot.UnitTests/packages.lock.json ================================================ { "version": 1, "dependencies": { "net10.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PJEdrZnnhvxIEXzDdvdZ38GvpdaiUfKkZ99kudS8riJwhowFb/Qh26Wjk9smrCWcYdMFQmpN5epGiL4o1s8LYA==" }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Microsoft.Reactive.Testing": { "type": "Direct", "requested": "[7.0.0-preview.1, )", "resolved": "7.0.0-preview.1", "contentHash": "5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==", "dependencies": { "System.Reactive": "7.0.0-preview.1" } }, "Nito.AsyncEx": { "type": "Direct", "requested": "[5.1.2, )", "resolved": "5.1.2", "contentHash": "hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==", "dependencies": { "Nito.AsyncEx.Context": "5.1.2", "Nito.AsyncEx.Coordination": "5.1.2", "Nito.AsyncEx.Interop.WaitHandles": "5.1.2", "Nito.AsyncEx.Oop": "5.1.2", "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Cancellation": "1.1.2" } }, "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Testing": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==", "dependencies": { "Polly.Core": "8.6.6" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "10.0.0" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "10.0.0", "Microsoft.Extensions.DependencyInjection": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "10.0.0", "Microsoft.Extensions.Http": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "fZzXogChrwQ/SfifQJgeW7AtR8hUv5+LH9oLWjm5OqfnVt3N8MwcMHHMdawvqqdjP79lIZgetnSpj77BLsSI1g==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "ODGomRlmt8/mFAqVyD9MgE4fXNkO6qDNeKuvmqNDuKjOL2UOkh/wJK0gEXS5VcViHFs+uQKOXD5xoTg1/ouKtA==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "wj8Vqtc3yDkTFo96Bnj8O9X70DYRNJayvPGg7wUUURhBHtH4zAbGgqG2RWrGgQKlrlUc/ZQGxzIZPskzXN2R4g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "WFwm63h4YhVOfEvTeieUGRKUz8nYKSd6mXC1vfqqr7ZW+b8mQBkaxMeAOvA2YFjjgRCKgVC72jhmxjLEDFwC4A==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "10.0.5", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "xjkxIPgrT0mKTfBwb+CVqZnRchyZgzKIfDQOp8z+WUC6vPe3WokIf71z+hJPkH0YBUYJwa7Z/al1R087ib9oiw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "10.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "SfK89ytD61S7DgzorFljSkUeluC1ncn6dtZgwc0ot39f/BEYWBl5jpgvodxduoYAs1d9HG8faCDRZxE95UMo2A==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "10.0.0", "contentHash": "r+mSvm/Ryc/iYcc9zcUG5VP9EBB8PL1rgVU6macEaYk45vmGRk9PntM3aynFKN6s3Q4WW36kedTycIctctpTUQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.0", "Microsoft.Extensions.Diagnostics": "10.0.0", "Microsoft.Extensions.Logging": "10.0.0", "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.Extensions.Options": "10.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "10.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Nito.AsyncEx.Context": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Coordination": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Collections.Deque": "1.1.1" } }, "Nito.AsyncEx.Interop.WaitHandles": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Oop": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==", "dependencies": { "Nito.AsyncEx.Coordination": "5.1.2" } }, "Nito.AsyncEx.Tasks": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Cancellation": { "type": "Transitive", "resolved": "1.1.2", "contentHash": "Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Collections.Deque": { "type": "Transitive", "resolved": "1.1.1", "contentHash": "CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==" }, "Nito.Disposables": { "type": "Transitive", "resolved": "2.2.1", "contentHash": "6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==" }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "7.0.0-preview.1", "contentHash": "eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[10.0.5, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[10.0.5, )", "Microsoft.AspNetCore.TestHost": "[10.0.5, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )" } } }, "net10.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net10.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } } }, "net8.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[8.0.25, )", "resolved": "8.0.25", "contentHash": "tKWAyIGm3eTKsJU0efxnx5dZhwvVZ0CGV73B0EJqSzSZrBY3pJN/P08haADl6TtVd13HusjuZe7V0nPOeyqHIg==", "dependencies": { "System.IO.Pipelines": "8.0.0" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Microsoft.Reactive.Testing": { "type": "Direct", "requested": "[7.0.0-preview.1, )", "resolved": "7.0.0-preview.1", "contentHash": "5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==", "dependencies": { "System.Reactive": "7.0.0-preview.1" } }, "Nito.AsyncEx": { "type": "Direct", "requested": "[5.1.2, )", "resolved": "5.1.2", "contentHash": "hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==", "dependencies": { "Nito.AsyncEx.Context": "5.1.2", "Nito.AsyncEx.Coordination": "5.1.2", "Nito.AsyncEx.Interop.WaitHandles": "5.1.2", "Nito.AsyncEx.Oop": "5.1.2", "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Cancellation": "1.1.2" } }, "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Testing": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==", "dependencies": { "Polly.Core": "8.6.6" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "8.0.0" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "nb6jCyxh5eP9bsXkHmGcDxUiVIl5wJSombl3LN2L+sjGEVXzcMKbdRe0fp8LQtuBM2hKXcXFxMAYdnohdYJF8Q==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "7.1.2" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "C6aPTFT5sJ+LhX8Vtbj4EfZ040YgItJLTksGbT+46pqhc0rGZggqlu4yPKQjLii75WSL/uVVcZVKNJwQzRPR5Q==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "HYtM1e8zKdNd44k+TEIm76O8hrbYsLj+yqKQwuO79wl0f6s+yHwcw0JStyaHLlbEE1kkbhtXeIEEC5YrauvxFA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "8.0.25", "contentHash": "eGWJa4xmc5054BHVwGGZWpfelv3I5H2cc8aFEe8Us6GyMamew7g78y/f3spEl5MYx4t4Hl8AelLMZ7Na0QG7uw==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "8.0.25", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "SydLwMRFx6EHPWJ+N6+MVaoArN1Htt92b935O3RUWPY1yUF63zEjvd3lBu79eWdZUwedP8TN2I5V9T3nackvIQ==", "dependencies": { "Microsoft.IdentityModel.Logging": "7.1.2", "Microsoft.IdentityModel.Tokens": "7.1.2" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "7.1.2", "contentHash": "6lHQoLXhnMQ42mGrfDkzbIOR3rzKM1W1tgTeMPLgLCqwwGw0d96xFi/UiX/fYsu7d6cD5MJiL3+4HuI8VU+sVQ==", "dependencies": { "Microsoft.IdentityModel.Protocols": "7.1.2", "System.IdentityModel.Tokens.Jwt": "7.1.2" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Nito.AsyncEx.Context": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Coordination": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Collections.Deque": "1.1.1" } }, "Nito.AsyncEx.Interop.WaitHandles": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Oop": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==", "dependencies": { "Nito.AsyncEx.Coordination": "5.1.2" } }, "Nito.AsyncEx.Tasks": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Cancellation": { "type": "Transitive", "resolved": "1.1.2", "contentHash": "Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Collections.Deque": { "type": "Transitive", "resolved": "1.1.1", "contentHash": "CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==" }, "Nito.Disposables": { "type": "Transitive", "resolved": "2.2.1", "contentHash": "6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==" }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "7.0.0-preview.1", "contentHash": "eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[8.0.25, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[8.0.25, )", "Microsoft.AspNetCore.TestHost": "[8.0.25, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net8.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net8.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0": { "Consul": { "type": "Direct", "requested": "[1.7.14.10, )", "resolved": "1.7.14.10", "contentHash": "7nYCLVHdJYxThVJ6Vo6wav3Qo6pVQ9o5PQn0Wbe+JA6/1hMfz3ymIAJYqj+jwQXoTixD4uuMTB+vEHPULShnwg==", "dependencies": { "Newtonsoft.Json": "13.0.1" } }, "coverlet.collector": { "type": "Direct", "requested": "[8.0.0, )", "resolved": "8.0.0", "contentHash": "EMkj/2F6n6IVPrvGYkqzGJs6phuGGkq6N+E7KW9rNyzNxXbwQ1KfMqWyXNf9nCNEQOA6IjFwmOLvkriwKE7Orw==" }, "Microsoft.AspNetCore.TestHost": { "type": "Direct", "requested": "[9.0.14, )", "resolved": "9.0.14", "contentHash": "4cHPhn6YoGhSpztc4k+zPmZBQ8maAChhlJsVQUBImXC/2iPkk9dG1U4HtKfhnZHyp/81bcTXWDY2E+jfONlrCg==" }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "jUEXmkBUPdOS/MP9areK/sbKhdklq9+tEhvwfxGalZVnmyLUO5rrheNNutUBtvbZ7J8ECkG7/r2KXi/IFC06cA==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.EnvironmentVariables": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "tchMGQ+zVTO40np/Zzg2Li/TIR8bksQgg4UVXZa0OzeFCKWnIYtxE2FVs+eSmjPGCjMS2voZbwN/mUcYfpSTuA==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.FileExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "OhTr0O79dP49734lLTqVveivVX9sDXxbI/8vjELAZTHXqoN90mdpgTAgwicJED42iaHMCcZcK6Bj+8wNyBikaw==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileProviders.Physical": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Json": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "brBM/WP0YAUYh2+QqSYVdK8eQHYQTtTEUJXJ+84Zkdo2buGLja9VSrMIhgoeBUU7JBmcskAib8Lb/N83bvxgYQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.FileExtensions": "10.0.5", "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "+XTMKQyDWg4ODoNHU/BN3BaI1jhGO7VCS+BnzT/4IauiG6y2iPAte7MyD7rHKS+hNP0TkFkjrae8DFjDUxtcxg==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5" } }, "Microsoft.Extensions.Logging.Console": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "PMs2gha2v24hvH5o5KQem5aNK4mN0BhhCWlMqsg9tzifWKzjeQi2tyPOP/RaWMVvalOhVLcrmoMYPqbnia/epg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Logging.Configuration": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "System.Text.Json": "10.0.5" } }, "Microsoft.Extensions.Logging.Debug": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "/VacEkBQ02A8PBXSa6YpbIXCuisYy6JJr62/+ANJDZE+RMBfZMcXJXLfr/LpyLE6pgdp17Wxlt7e7R9zvkwZ3Q==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Direct", "requested": "[10.0.5, )", "resolved": "10.0.5", "contentHash": "BB9uUW3+6Rxu1R97OB1H/13lUF8P2+H1+eDhpZlK30kDh/6E4EKHBUqTp+ilXQmZLzsRErxON8aBSR6WpUKJdg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[18.3.0, )", "resolved": "18.3.0", "contentHash": "xW3kXuWRQtgoxJp4J+gdhHSQyK+6Wb/AZDSd7lMvuMRYlZ1tnpkojyfZlWilB5G4dmZ0Y0ZxU/M23TlubndNkw==", "dependencies": { "Microsoft.CodeCoverage": "18.3.0", "Microsoft.TestPlatform.TestHost": "18.3.0" } }, "Microsoft.Reactive.Testing": { "type": "Direct", "requested": "[7.0.0-preview.1, )", "resolved": "7.0.0-preview.1", "contentHash": "5YUO6KVeYCGn7cAULeXLfUv17cjWJ2bdGH31PxS71UJ/F+VRm0KGEKqiWkKRIwPvjRBeDHAkgKe/aGDA/OnyLA==", "dependencies": { "System.Reactive": "7.0.0-preview.1" } }, "Nito.AsyncEx": { "type": "Direct", "requested": "[5.1.2, )", "resolved": "5.1.2", "contentHash": "hq+N63M/2znx2z1VzvPDHNg+HIWKdIloEZre+P7E0O+2iRf1Q4HBOgeiJU6SzFD/fWoyKyKSSSrekk4RgiXaeQ==", "dependencies": { "Nito.AsyncEx.Context": "5.1.2", "Nito.AsyncEx.Coordination": "5.1.2", "Nito.AsyncEx.Interop.WaitHandles": "5.1.2", "Nito.AsyncEx.Oop": "5.1.2", "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Cancellation": "1.1.2" } }, "Polly": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "czKHYJ6uGowPijuZt4kgF4njfGvWxVZ8mKBcrZ9iEtwDe9HKdF0ug6p6TwUG8EHuuufgbDU//rSBFebt5/0Fyw==", "dependencies": { "Polly.Core": "8.6.6" } }, "Polly.Testing": { "type": "Direct", "requested": "[8.6.6, )", "resolved": "8.6.6", "contentHash": "wQjrM2LGlqzCTOmsH57/ueoLi7HIyWwsROdiqKbHjy0ojLyQVK68RbE3QmvuOAGCeU+YZAOvyhRT7vf9weqhRQ==", "dependencies": { "Polly.Core": "8.6.6" } }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", "requested": "[8.16.0, )", "resolved": "8.16.0", "contentHash": "rrs2u7DRMXQG2yh0oVyF/vLwosfRv20Ld2iEpYcKwQWXHjfV+gFXNQsQ9p008kR9Ou4pxBs68Q6/9zC8Gi1wjg==", "dependencies": { "Microsoft.IdentityModel.JsonWebTokens": "8.16.0", "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "xunit.runner.visualstudio": { "type": "Direct", "requested": "[3.1.5, )", "resolved": "3.1.5", "contentHash": "tKi7dSTwP4m5m9eXPM2Ime4Kn7xNf4x4zT9sdLO/G4hZVnQCRiMTWoSZqI/pYTVeI27oPPqHBKYI/DjJ9GsYgA==" }, "xunit.v3": { "type": "Direct", "requested": "[3.2.2, )", "resolved": "3.2.2", "contentHash": "L+4/4y0Uqcg8/d6hfnxhnwh4j9FaeULvefTwrk30rr1o4n/vdPfyUQ8k0yzH8VJx7bmFEkDdcRfbtbjEHlaYcA==", "dependencies": { "xunit.v3.mtp-v1": "[3.2.2]" } }, "BouncyCastle.Cryptography": { "type": "Transitive", "resolved": "2.4.0", "contentHash": "SwXsAV3sMvAU/Nn31pbjhWurYSjJ+/giI/0n6tCrYoupEK34iIHCuk3STAd9fx8yudM85KkLSVdn951vTng/vQ==" }, "Castle.Core": { "type": "Transitive", "resolved": "5.1.1", "contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==", "dependencies": { "System.Diagnostics.EventLog": "6.0.0" } }, "DiffEngine": { "type": "Transitive", "resolved": "11.3.0", "contentHash": "k0ZgZqd09jLZQjR8FyQbSQE86Q7QZnjEzq1LPHtj1R2AoWO8sjV5x+jlSisL7NZAbUOI4y+7Bog8gkr9WIRBGw==", "dependencies": { "EmptyFiles": "4.4.0", "System.Management": "6.0.1" } }, "EmptyFiles": { "type": "Transitive", "resolved": "4.4.0", "contentHash": "gwJEfIGS7FhykvtZoscwXj/XwW+mJY6UbAZk+qtLKFUGWC95kfKXnj8VkxsZQnWBxJemM/q664rGLN5nf+OHZw==" }, "FluentValidation": { "type": "Transitive", "resolved": "12.1.1", "contentHash": "EPpkIe1yh1a0OXyC100oOA8WMbZvqUu5plwhvYcb7oSELfyUZzfxV48BLhvs3kKo4NwG7MGLNgy1RJiYtT8Dpw==" }, "IPAddressRange": { "type": "Transitive", "resolved": "6.3.0", "contentHash": "VrGoeUz+ZK2QiwHNj+vab9uOvTDucenRseJZjc4uB7ASduQ7RNWnpd8gy1e9z2BsY4VoigVaCRrcQCQKuQVSiw==" }, "KubeClient": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "LPcQzwfwZ/lwq3gXBzaoX5Kl4yHFMoYVprqzg+LO2eiH1kGxUQenCP4L3PVmBuvGPPdV7gCbRYgqWEVno75ZIg==", "dependencies": { "KubeClient.Core": "3.1.1", "KubeClient.Http": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Core": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "mmoPmkbbJe9JYU1dd9NFenB3Ovd9syqiMhVs5evANeePLLT+z1sjypjfPn9QoedGwXbcTdMk5D5ysFV9Oq18wQ==", "dependencies": { "Microsoft.Extensions.Logging": "9.0.3" } }, "KubeClient.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "Ip3j5bbWEjUc9nK4XWC/OtmrDxfBF0iZ/cuRojkuebhIxporSZvXJVmJxK09fCb6NSiS0dn+6/RPyPu199RUXg==", "dependencies": { "KubeClient": "3.1.1", "KubeClient.Extensions.KubeConfig": "3.1.1", "Microsoft.Extensions.Configuration.Binder": "9.0.3", "Microsoft.Extensions.DependencyInjection": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "KubeClient.Extensions.KubeConfig": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "gHwW2SubrB1tukFZ3K5xgRAowkZh4JQZrzNM64WE4HfI1xVyY3FxEKIxogzK39Y15tbnWz9DjuiJ2RKtCN5wMQ==", "dependencies": { "BouncyCastle.Cryptography": "2.4.0", "KubeClient": "3.1.1", "Newtonsoft.Json": "13.0.3", "System.Reactive": "6.0.1", "YamlDotNet": "16.1.3" } }, "KubeClient.Http": { "type": "Transitive", "resolved": "3.1.1", "contentHash": "jta97xQm/ZxwrD/9agZa87NCvCBjUSxV2XzejemkLXkKvAybEiRFtXFU7qMt9SvjNkpgiLhl1Cn4Idh0lmpZNA==", "dependencies": { "KubeClient.Core": "3.1.1", "Microsoft.AspNetCore.JsonPatch": "9.0.3", "Microsoft.Extensions.Http": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.ApplicationInsights": { "type": "Transitive", "resolved": "2.23.0", "contentHash": "nWArUZTdU7iqZLycLKWe0TDms48KKGE6pONH2terYNa8REXiqixrMOkf1sk5DHGMaUTqONU2YkS4SAXBhLStgw==" }, "Microsoft.AspNetCore.Authentication.JwtBearer": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "CHG/cxMJa3Peh5PYqJPLPHdwaGjXcoCmD1mUjo4xH2HilA6K0DKoVEr5ollVCqkQDGGutEfkzab10r8+pSeuMQ==", "dependencies": { "Microsoft.IdentityModel.Protocols.OpenIdConnect": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "aNrZcz0+FAw1wwOtsTpP+nYvDIFtKnMmfC+gOzUcf1moqyJdlPyoQZcIbnxu0xyPnfnolvr9wYiDM5w/peQsvg==", "dependencies": { "Newtonsoft.Json": "13.0.3" } }, "Microsoft.AspNetCore.MiddlewareAnalysis": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "036P2G2dp+ktc1y04dc6QW/0jlXqHcc32fm9NdG+RqZbEp9YYA8YpV9d2OG9/p0kgr7TSlhBawUgooOEHlw5HA==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.14" } }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Transitive", "resolved": "9.0.14", "contentHash": "/Da05WZ7xMcXiZd4eiMuAQncXIWq0cGW7a1o/1WGaJsmPg7Md5GepinDFmOipuVF2d9HHailV30w15uNCb/ZdQ==", "dependencies": { "Microsoft.AspNetCore.JsonPatch": "9.0.14", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" }, "Microsoft.CodeCoverage": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "23BNy/vziREC20Wwhb50K7+kZe0m07KlLWDQv4qjJ9tt3QjpDpDIqJFrhYHmMEo9xDkuSp55U/8h4bMF7MiB+g==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "k/QDdQ94/0Shi0KfU+e12m73jfQo+3JpErTtgpZfsCIqkvdEEO0XIx6R+iTbN55rNPaNhOqNY4/sB+jZ8XxVPw==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8Rx5sqg04FttxrumyG6bmoRuFRgYzK6IVwF1i0/o0cXfKBdDeVpJejKHtJCMjyg9E/DNMVqpqOGe/tCT5gYvVA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "P09QpTHjqHmCLQOTC+WyLkoRNxek4NIvfWt+TnU0etoDUSRxcltyd6+j/ouRbMdLR0j44GqGO+lhI2M4fAHG4g==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "99Z4rjyXopb1MIazDSPcvwYCUdYNO01Cf1GUs2WUjIFAbkGmwzj2vPa2k+3pheJRV+YgNd2QqRKHAri0oBAU4Q==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5" } }, "Microsoft.Extensions.Configuration.CommandLine": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "NZuZMz3Q8Z780nKX3ifV1fE7lS+6pynDHK71OfU4OZ1ItgvDOhyOC7E6z+JMZrAj63zRpwbdldYFk499t3+1dQ==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Configuration.UserSecrets": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ihDHu2dJYQird9pl2CbdwuNDfvCZdOS0S7SPlNfhPt0B81UTT+yyZKz2pimFZGUp3AfuBRnqUCxB2SjsZKHVUw==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "v1SVsowG6YE1YnHVGmLWz57YTRCQRx9pH5ebIESXfm5isI9gA3QaMyg/oMTzPpXYZwSAVDzYItGJKfmV+pqXkQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "iVMtq9eRvzyhx8949EGT0OCYJfXi737SbRVzWXE5GrOgGj5AaZ9eUuxA/BSUfmOMALKn/g8KfFaNQw0eiB3lyA==" }, "Microsoft.Extensions.DiagnosticAdapter": { "type": "Transitive", "resolved": "3.1.32", "contentHash": "oDv3wt+Q5cmaSfOQ3Cdu6dF6sn/x5gzWdNpOq4ajBwCMWYBr6CchncDvB9pF83ORlbDuX32MsVLOPGPxW4Lx4g==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "gqhbIq6adm0+/9IlDYmchekoxNkmUTm7rfTG3k4zzoQkjRuD8TQGwL1WnIcTDt4aQ+j+Vu0OQrjI8GlpJQQhIA==", "dependencies": { "Microsoft.Extensions.Configuration": "9.0.3", "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.3", "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.3" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "/fn0Xe8t+3YbMfwyTk4hFirWyAG1pBA5ogVYsrKAuuD2gbqOWhFuSA28auCmS3z8Y2eq3miDIKq4pFVRWA+J6g==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "nCBmCx0Xemlu65ZiWMcXbvfvtznKxf4/YYKF9R28QkqdI9lTikedGqzJ28/xmdGGsxUnsP5/3TQGpiPwVjK0dA==", "dependencies": { "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "dMu5kUPSfol1Rqhmr6nWPSmbFjDe9w6bkoKithG17bWTZA0UyKirTatM5mqYUN3mGpNA0MorlusIoVTh6J7o5g==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "10.0.5", "Microsoft.Extensions.FileSystemGlobbing": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.FileSystemGlobbing": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "mOE3ARusNQR0a5x8YOcnUbfyyXGqoAWQtEc7qFOfNJgruDWQLo39Re+3/Lzj5pLPFuFYj8hN4dgKzaSQDKiOCw==" }, "Microsoft.Extensions.Hosting": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "ItYHpdqVp5/oFLT5QqbopnkKlyFG9EW/9nhM6/yfObeKt6Su0wkBio6AizgRHGNwhJuAtlE5VIjow5JOTrip6w==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.Configuration.FileExtensions": "8.0.0", "Microsoft.Extensions.Configuration.Json": "8.0.0", "Microsoft.Extensions.Configuration.UserSecrets": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Physical": "8.0.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Logging.Debug": "8.0.0", "Microsoft.Extensions.Logging.EventLog": "8.0.0", "Microsoft.Extensions.Logging.EventSource": "8.0.0", "Microsoft.Extensions.Options": "8.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0" } }, "Microsoft.Extensions.Http": { "type": "Transitive", "resolved": "9.0.3", "contentHash": "rwChgI3lPqvUzsCN3egSW/6v4kP9/RQ2QrkZUwyAiHiwEoIB6QbYkATNvUsgjV6nfrekocyciCzy53ZFRuSaHA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "9.0.3", "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.3", "Microsoft.Extensions.Diagnostics": "9.0.3", "Microsoft.Extensions.Logging": "9.0.3", "Microsoft.Extensions.Logging.Abstractions": "9.0.3", "Microsoft.Extensions.Options": "9.0.3" } }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "9HOdqlDtPptVcmKAjsQ/Nr5Rxfq6FMYLdhvZh1lVmeKR738qeYecQD7+ldooXf+u2KzzR1kafSphWngIM3C6ug==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "System.Diagnostics.DiagnosticSource": "10.0.5" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "cSgxsDgfP0+gmVRPVoNHI/KIDavIZxh+CxE6tSLPlYTogqccDnjBFI9CgEsiNuMP6+fiuXUwhhlTz36uUEpwbQ==", "dependencies": { "Microsoft.Extensions.Configuration": "10.0.5", "Microsoft.Extensions.Configuration.Abstractions": "10.0.5", "Microsoft.Extensions.Configuration.Binder": "10.0.5", "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Logging": "10.0.5", "Microsoft.Extensions.Logging.Abstractions": "10.0.5", "Microsoft.Extensions.Options": "10.0.5", "Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.5" } }, "Microsoft.Extensions.Logging.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "3X9D3sl7EmOu7vQp5MJrmIJBl5XSdOhZPYXUeFfYa6Nnm9+tok8x3t3IVPLhm7UJtPOU61ohFchw8rNm9tIYOQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "System.Diagnostics.EventLog": "8.0.0" } }, "Microsoft.Extensions.Logging.EventSource": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "oKcPMrw+luz2DUAKhwFXrmFikZWnyc8l2RKoQwqU3KIZZjcfoJE0zRHAnqATfhRZhtcbjl/QkiY2Xjxp0xu+6w==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.Extensions.Options": "8.0.0", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "MDaQMdUplw0AIRhWWmbLA7yQEXaLIHb+9CTroTiNS8OlI0LMXS4LCxtopqauiqGCWlRgJ+xyraVD8t6veRAFbw==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.5", "Microsoft.Extensions.Primitives": "10.0.5" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "/HUHJ0tw/LQvD0DZrz50eQy/3z7PfX7WWEaXnjKTV9/TNdcgFlNTZGo49QhS7PTmhDqMyHRMqAXSBxLh0vso4g==" }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "gSxKLWRZzBpIsEoeUPkxfywNCCvRvl7hkq146XHPk5vOQc9izSf1I+uL1vh4y2U19QPxd9Z8K/8AdWyxYz2lSg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "prBU72cIP4V8E9fhN+o/YdskTsLeIcnKPbhZf0X6mD7fdxoZqnS/NdEkSr+9Zp+2q7OZBOMfNBKGbTbhXODO4w==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.16.0" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "MTzXmETkNQPACR7/XCXM1OGM6oU9RkyibqeJRtO9Ndew2LnGjMf9Atqj2VSf4XC27X0FQycUAlzxxEgQMWn2xQ==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "8.16.0" } }, "Microsoft.IdentityModel.Protocols": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "uA2vpKqU3I2mBBEaeJAWPTjT9v1TZrGWKdgK6G5qJd03CLx83kdiqO9cmiK8/n1erkHzFBwU/RphP83aAe3i3g==", "dependencies": { "Microsoft.IdentityModel.Tokens": "8.0.1" } }, "Microsoft.IdentityModel.Protocols.OpenIdConnect": { "type": "Transitive", "resolved": "8.0.1", "contentHash": "AQDbfpL+yzuuGhO/mQhKNsp44pm5Jv8/BI4KiFXR7beVGZoSH35zMV3PrmcfvSTsyI6qrcR898NzUauD6SRigg==", "dependencies": { "Microsoft.IdentityModel.Protocols": "8.0.1", "System.IdentityModel.Tokens.Jwt": "8.0.1" } }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", "resolved": "8.16.0", "contentHash": "rtViGJcGsN7WcfUNErwNeQgjuU5cJNl6FDQsfi9TncwO+Epzn0FTfBsg3YuFW1Q0Ch/KPxaVdjLw3/+5Z5ceFQ==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.0", "Microsoft.IdentityModel.Logging": "8.16.0" } }, "Microsoft.Testing.Extensions.Telemetry": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "No5AudZMmSb+uNXjlgL2y3/stHD2IT4uxqc5yHwkE+/nNux9jbKcaJMvcp9SwgP4DVD8L9/P3OUz8mmmcvEIdQ==", "dependencies": { "Microsoft.ApplicationInsights": "2.23.0", "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Extensions.TrxReport.Abstractions": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "AL46Xe1WBi85Ntd4mNPvat5ZSsZ2uejiVqoKCypr8J3wK0elA5xJ3AN4G/Q4GIwzUFnggZoH/DBjnr9J18IO/g==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.Testing.Platform": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "QafNtNSmEI0zazdebnsIkDKmFtTSpmx/5PLOjURWwozcPb3tvRxzosQSL8xwYNM1iPhhKiBksXZyRSE2COisrA==" }, "Microsoft.Testing.Platform.MSBuild": { "type": "Transitive", "resolved": "1.9.1", "contentHash": "oTUtyR4X/s9ytuiNA29FGsNCCH0rNmY5Wdm14NCKLjTM1cT9edVSlA+rGS/mVmusPqcP0l/x9qOnMXg16v87RQ==", "dependencies": { "Microsoft.Testing.Platform": "1.9.1" } }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "AEIEX2aWdPO9XbtR96eBaJxmXRD9vaI9uQ1T/JbPEKlTAZwYx0ZrMzKyULMdh/HH9Sg03kXCoN7LszQ90o6nPQ==" }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", "resolved": "18.3.0", "contentHash": "twmsoelXnp1uWMU3VGip9f0Jr1mZ0PZqgJdF35CIrdYgYrkHIJMV1m8uKyhcdjLdsQDESHAgkR7KhS9i1qpJag==", "dependencies": { "Microsoft.TestPlatform.ObjectModel": "18.3.0", "Newtonsoft.Json": "13.0.3" } }, "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "Moq": { "type": "Transitive", "resolved": "4.20.72", "contentHash": "EA55cjyNn8eTNWrgrdZJH5QLFp2L43oxl1tlkoYUKIE9pRwL784OWiTXeCV5ApS+AMYEAlt7Fo03A2XfouvHmQ==", "dependencies": { "Castle.Core": "5.1.1" } }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.3", "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "Newtonsoft.Json.Bson": { "type": "Transitive", "resolved": "1.0.2", "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", "dependencies": { "Newtonsoft.Json": "12.0.1" } }, "Nito.AsyncEx.Context": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "rMwL7Nj3oNyvFu/jxUzQ/YBobEkM2RQHe+5mpCDRyq6mfD7vCj7Z3rjB6XgpM6Mqcx1CA2xGv0ascU/2Xk8IIg==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Coordination": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "QMyUfsaxov//0ZMbOHWr9hJaBFteZd66DV1ay4J5wRODDb8+K/uHC7+3VsOflo6SVw/29mu8OWZp8vMDSuzc0w==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2", "Nito.Collections.Deque": "1.1.1" } }, "Nito.AsyncEx.Interop.WaitHandles": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "qym29lFBCSIacKvFcJDW+beXzuO+6y9lWdd1KecxzzAqtNuvlYgNPwIsxwdhEINLhTT4aDuCM3JalpUZYWI51Q==", "dependencies": { "Nito.AsyncEx.Tasks": "5.1.2" } }, "Nito.AsyncEx.Oop": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "MxQl/NFoPgMApyjbB2fSZBrjdf9r6ODd/BTrWLyJKYX6UeNfw0Ocr0cPiTg2LRN0Ayud8Gj4dh67AdasNn709Q==", "dependencies": { "Nito.AsyncEx.Coordination": "5.1.2" } }, "Nito.AsyncEx.Tasks": { "type": "Transitive", "resolved": "5.1.2", "contentHash": "jEkCfR2/M26OK/U4G7SEN063EU/F4LiVA06TtpZILMdX/quIHCg+wn31Zerl2LC+u1cyFancjTY3cNAr2/89PA==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Cancellation": { "type": "Transitive", "resolved": "1.1.2", "contentHash": "Z+SZKp0KxMC6tEVbXe8ah4pBJadyqP0pObQMaZcBavhIDEIsGuxt7PL+B9AiNJD3Ni5VgnZsnii5HPJgVDE81w==", "dependencies": { "Nito.Disposables": "2.2.1" } }, "Nito.Collections.Deque": { "type": "Transitive", "resolved": "1.1.1", "contentHash": "CU0/Iuv5VDynK8I8pDLwkgF0rZhbQoZahtodfL0M3x2gFkpBRApKs8RyMyNlAi1mwExE4gsmqQXk4aFVvW9a4Q==" }, "Nito.Disposables": { "type": "Transitive", "resolved": "2.2.1", "contentHash": "6sZ5uynQeAE9dPWBQGKebNmxbY4xsvcc5VplB5WkYEESUS7oy4AwnFp0FhqxTSKm/PaFrFqLrYr696CYN8cugg==" }, "Polly.Core": { "type": "Transitive", "resolved": "8.6.6", "contentHash": "lCBL9mmhF9TZxHG3beVRkyjlLohkIC464xIAq7J7Y59C+z42hmsdUaeCKl2SIAYertOUU5TeBXyQDLDQGIKePQ==" }, "Shouldly": { "type": "Transitive", "resolved": "4.3.0", "contentHash": "sDetrWXrl6YXZ4HeLsdBoNk3uIa7K+V4uvIJ+cqdRa5DrFxeTED7VkjoxCuU1kJWpUuBDZz2QXFzSxBtVXLwRQ==", "dependencies": { "DiffEngine": "11.3.0", "EmptyFiles": "4.4.0" } }, "Steeltoe.Common": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "hwTApMg/TnX1imTvGRbhth8dHe3AUWD/MxKXK0kqEE84mEPjK4IDHJiV38Mx4+mH13x6xbipeCKte+KdadaqLQ==", "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Configuration.CommandLine": "8.0.0", "Microsoft.Extensions.Configuration.EnvironmentVariables": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "System.Reflection.MetadataLoadContext": "4.6.0" } }, "Steeltoe.Common.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "86nxnq4Wd6MQFz8ZSfYwvgg8RocfmZAOGxS8sHCspB2pTkmMsqdQBhpI3yufZBYXxqt48EIXuzBqZCSAr1ggJg==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0" } }, "Steeltoe.Common.Http": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "KuEKWfx2yubvbVzq5c/1rTBusL/FvLXA2w93jBfi5KQn1D0F9c6R03wLgpMFM46j0KxGZF5iKxm00g/8GHETlQ==", "dependencies": { "Microsoft.Extensions.Http": "3.1.0", "Steeltoe.Common": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Connector.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "35thB2pyX5nY9RFFDlRwmyee/qYA5OgPr6YkhmDnjCA515VLz6uV/74CHwi4urBbmfP1LX74XnrDyZhxMYQDHg==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0", "Steeltoe.Extensions.Configuration.Abstractions": "3.3.0" } }, "Steeltoe.Connector.ConnectorBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "tMSiExMaHuI6+wmZ/XoIrnYNY1qWwqCCPTk5Zvuu9b2FbdNNNd/Awpq34r70V/2bYswxUwXRgcOAESyfRn+e+w==", "dependencies": { "Steeltoe.Common": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "4cqyNvuzPo/Obr3bkEn/d6yntSkPJa5iuaR5Htu1O70iRQU2MFa1UOLDKBl/u3G4L0FU27EHqY49GHyS+0czMA==", "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientBase": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "yZzshF4tuzD2ge4kA/drUN6MdjfafVcJen83ohrcsP6jiLaNweabPR5WTjK9dfVMyot8t2FjsLMXKcYx/NnsFw==", "dependencies": { "Microsoft.Extensions.Hosting": "8.0.0", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0", "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.ConnectorBase": "3.3.0", "Steeltoe.Discovery.Abstractions": "3.3.0" } }, "Steeltoe.Discovery.ClientCore": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "QsmXGfgjTPdj/GW4X51U7q1im5maOpMBe6sbuzMiqXCy890XS4j5jIFtXkNZRGzXm1IW0NuDe9+iwS7HnrRp2g==", "dependencies": { "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Discovery.Eureka": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "P16nX9nlK+CDJRJz9room/JZrR0UvkprOz185d1NTsnlfd6BrBIrhpJIqjwZXUveT6bWf8hcNI5Y+sR8sE/vjA==", "dependencies": { "Steeltoe.Common.Http": "3.3.0", "Steeltoe.Connector.Abstractions": "3.3.0", "Steeltoe.Discovery.ClientBase": "3.3.0" } }, "Steeltoe.Extensions.Configuration.Abstractions": { "type": "Transitive", "resolved": "3.3.0", "contentHash": "8hMFGX21iAt+OibwFMr8LKnB6zSS372cOMz4A2X4K4dDsKjISZYwaXWoEzDmxuZn6HHywsQhIhR4//2bUPhsFA==", "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.DependencyInjection": "8.0.0", "Steeltoe.Common.Abstractions": "3.3.0" } }, "System.CodeDom": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "CCbzHQ26L3jskdwHh+4bxxW84lUMIrAAmeSlpO69AlrQV0DKbj1/I+feLaLSuZeqXPr9UlSy0OcgZoXOk2a6/g==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.IO.Pipelines": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "8/ZHN/j2y1t+7McdCf1wXku2/c7wtrGLz3WQabIoPuLAn3bHDWT6YOJYreJq8sCMPSo6c8iVYXUdLlFGX5PEqw==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Reactive": { "type": "Transitive", "resolved": "7.0.0-preview.1", "contentHash": "eRkTG0pCaU3pOKt19ZeoNXvTsuIsLAYCF8VIqIU1y+mmtNOJiDYhmw2iyOKxGqHFNSeeiMd5cYp8IN5lEImvhw==" }, "System.Reflection.MetadataLoadContext": { "type": "Transitive", "resolved": "4.6.0", "contentHash": "TezS9fEP9kzL5U6GYHZY6I/tqz6qiHKNgAzuT6JJXJXuP+wWvNLN03gPxBK2uLP0LrLg/QXEAF++lxBNBSYILA==" }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" }, "System.Text.Json": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "vW2zhkWziyfhoSXNf42mTWyilw+vfwBGOsODDsHSFtOIY6LCgfRVUyaAilLEL4Kc1fzhaxcep5pS0VWYPSDW0w==", "dependencies": { "System.IO.Pipelines": "10.0.5", "System.Text.Encodings.Web": "10.0.5" } }, "xunit.analyzers": { "type": "Transitive", "resolved": "1.27.0", "contentHash": "y/pxIQaLvk/kxAoDkZW9GnHLCEqzwl5TW0vtX3pweyQpjizB9y3DXhb9pkw2dGeUqhLjsxvvJM1k89JowU6z3g==" }, "xunit.v3.assert": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "BPciBghgEEaJN/JG00QfCYDfEfnLgQhfnYEy+j1izoeHVNYd5+3Wm8GJ6JgYysOhpBPYGE+sbf75JtrRc7jrdA==" }, "xunit.v3.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Hj775PEH6GTbbg0wfKRvG2hNspDCvTH9irXhH4qIWgdrOSV1sQlqPie+DOvFeigsFg2fxSM3ZAaaCDQs+KreFA==", "dependencies": { "Microsoft.Bcl.AsyncInterfaces": "6.0.0" } }, "xunit.v3.core.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "Ga5aA2Ca9ktz+5k3g5ukzwfexwoqwDUpV6z7atSEUvqtd6JuybU1XopHqg1oFd78QdTfZgZE9h5sHpO4qYIi5w==", "dependencies": { "Microsoft.Testing.Extensions.Telemetry": "1.9.1", "Microsoft.Testing.Extensions.TrxReport.Abstractions": "1.9.1", "Microsoft.Testing.Platform": "1.9.1", "Microsoft.Testing.Platform.MSBuild": "1.9.1", "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.inproc.console": "[3.2.2]" } }, "xunit.v3.extensibility.core": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "srY8z/oMPvh/t8axtO2DwrHajhFMH7tnqKildvYrVQIfICi8fOn3yIBWkVPAcrKmHMwvXRJ/XsQM3VMR6DOYfQ==", "dependencies": { "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.mtp-v1": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "O41aAzYKBT5PWqATa1oEWVNCyEUypFQ4va6K0kz37dduV3EKzXNMaV2UnEhufzU4Cce1I33gg0oldS8tGL5I0A==", "dependencies": { "xunit.analyzers": "1.27.0", "xunit.v3.assert": "[3.2.2]", "xunit.v3.core.mtp-v1": "[3.2.2]" } }, "xunit.v3.runner.common": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "/hkHkQCzGrugelOAehprm7RIWdsUFVmIVaD6jDH/8DNGCymTlKKPTbGokD5czbAfqfex47mBP0sb0zbHYwrO/g==", "dependencies": { "Microsoft.Win32.Registry": "[5.0.0]", "xunit.v3.common": "[3.2.2]" } }, "xunit.v3.runner.inproc.console": { "type": "Transitive", "resolved": "3.2.2", "contentHash": "ulWOdSvCk+bPXijJZ73bth9NyoOHsAs1ZOvamYbCkD4DNLX/Bd29Ve2ZNUwBbK0MqfIYWXHZViy/HKrdEC/izw==", "dependencies": { "xunit.v3.extensibility.core": "[3.2.2]", "xunit.v3.runner.common": "[3.2.2]" } }, "YamlDotNet": { "type": "Transitive", "resolved": "16.1.3", "contentHash": "gtHGiDvU9VTtWte8f0thIM38cL1oowOjStKpeAEKKfA+Rc4AvekJzqFDZiiPcc4kw00ZiwR4OTJS56L16q98DQ==" }, "ocelot": { "type": "Project", "dependencies": { "FluentValidation": "[12.1.1, )", "IPAddressRange": "[6.3.0, )", "Microsoft.AspNetCore.MiddlewareAnalysis": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.Extensions.DiagnosticAdapter": "[3.1.32, )" } }, "ocelot.provider.consul": { "type": "Project", "dependencies": { "Consul": "[1.7.14.10, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.eureka": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Steeltoe.Discovery.ClientCore": "[3.3.0, )", "Steeltoe.Discovery.Eureka": "[3.3.0, )" } }, "ocelot.provider.kubernetes": { "type": "Project", "dependencies": { "KubeClient": "[3.1.1, )", "KubeClient.Extensions.DependencyInjection": "[3.1.1, )", "Ocelot": "[0.0.0-dev, )" } }, "ocelot.provider.polly": { "type": "Project", "dependencies": { "Ocelot": "[0.0.0-dev, )", "Polly": "[8.6.6, )" } }, "ocelot.testing": { "type": "Project", "dependencies": { "Microsoft.AspNetCore.Authentication.JwtBearer": "[9.0.14, )", "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "[9.0.14, )", "Microsoft.AspNetCore.TestHost": "[9.0.14, )", "Moq": "[4.20.72, )", "Ocelot": "[0.0.0-dev, )", "Shouldly": "[4.3.0, )", "System.Text.Json": "[10.0.5, )" } } }, "net9.0/osx-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } }, "net9.0/win-x64": { "Microsoft.Win32.Registry": { "type": "Transitive", "resolved": "5.0.0", "contentHash": "dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==" }, "System.Diagnostics.EventLog": { "type": "Transitive", "resolved": "8.0.0", "contentHash": "fdYxcRjQqTTacKId/2IECojlDSFvp7LP5N78+0z/xH7v/Tuw5ZAxu23Y6PTCRinqyu2ePx+Gn1098NC6jM6d+A==" }, "System.Management": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "10J1D0h/lioojphfJ4Fuh5ZUThT/xOVHdV9roGBittKKNP2PMjrvibEdbVTGZcPra1399Ja3tqIJLyQrc5Wmhg==", "dependencies": { "System.CodeDom": "6.0.0" } }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "10.0.5", "contentHash": "opvD/nKTzGKA7GVntZ9L823kN6IxgHQfuxY+VI9gv8VE1Y7CSKoi/QS1EYDQiA63MqtZsD7X6zkISd2ZQJohTQ==" } } } } ================================================ FILE: testing/AcceptanceSteps.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Ocelot.Middleware; using Shouldly; using System.Diagnostics; using System.Net; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Unicode; using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; namespace Ocelot.Testing; /// /// This is the base class for acceptance testing classes, specifically for developing Step classes. /// It is the recommended base class to inherit from. /// public class AcceptanceSteps : IDisposable { protected IHost? ocelotHost; protected TestServer? ocelotServer; protected HttpClient? ocelotClient; protected HttpResponseMessage? response; private readonly Guid _testId; protected readonly Random random; protected readonly string ocelotConfigFileName; protected readonly ServiceHandler handler; public AcceptanceSteps() { _testId = Guid.NewGuid(); random = new Random(); ocelotConfigFileName = $"ocelot-{_testId:N}.json"; // {ConfigurationBuilderExtensions.PrimaryConfigFile}"; Files = [ocelotConfigFileName]; Folders = []; handler = new(); } protected List Files { get; } protected List Folders { get; } protected virtual string TestID { get => _testId.ToString("N"); } public virtual string Body([CallerMemberName] string? responseBody = null) => responseBody ?? GetType().Name; public virtual string TestName([CallerMemberName] string? testName = null) => testName ?? GetType().Name; // but it could be TestID also public HttpClient? OcelotClient => ocelotClient; protected virtual FileHostAndPort Localhost(int port) => new("localhost", port); protected static string DownstreamUrl(int port) => DownstreamUrl(port, Uri.UriSchemeHttp); protected static string DownstreamUrl(int port, string scheme) => $"{scheme ?? Uri.UriSchemeHttp}://localhost:{port}"; protected static string LoopbackLocalhostUrl(int port, int loopbackIndex = 0) => $"{Uri.UriSchemeHttp}://127.0.0.{++loopbackIndex}:{port}"; public virtual FileConfiguration GivenConfiguration(params FileRoute[] routes) { var c = new FileConfiguration(); c.Routes.AddRange(routes); return c; } public virtual FileRoute GivenDefaultRoute(int port) => GivenRoute(port); public virtual FileRoute GivenCatchAllRoute(int port) => GivenRoute(port, "/{everything}", "/{everything}"); public virtual FileRoute GivenRoute(int port, string? upstream = null, string? downstream = null) { var r = new FileRoute(); r.DownstreamHostAndPorts.Add(Localhost(port)); r.DownstreamPathTemplate = downstream ?? "/"; r.DownstreamScheme = Uri.UriSchemeHttp; r.UpstreamHttpMethod.Add(HttpMethods.Get); r.UpstreamPathTemplate = upstream ?? "/"; return r; } public virtual void GivenThereIsAConfiguration(FileConfiguration configuration) => GivenThereIsAConfiguration(configuration, ocelotConfigFileName); public virtual void GivenThereIsAConfiguration(FileConfiguration from, string toFile) { var json = SerializeJson(from, ref toFile); File.WriteAllText(toFile, json); } public virtual Task GivenThereIsAConfigurationAsync(FileConfiguration from, string toFile) { var json = SerializeJson(from, ref toFile); return File.WriteAllTextAsync(toFile, json); } protected virtual string SerializeJson(FileConfiguration from, ref string toFile) { toFile ??= ocelotConfigFileName; Files.Add(toFile); // register for disposing return JsonSerializer.Serialize(from, JsonWebIndented /*Formatting.Indented*/); } public readonly static JsonSerializerOptions JsonWebIndented = new() { Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), // Avoid escaping non-ASCII PropertyNamingPolicy = JsonNamingPolicy.CamelCase, // Use camelCase for web WriteIndented = true, // Not compact output for better readability PropertyNameCaseInsensitive = true // Optional: for deserialization }; #region GivenOcelotIsRunning public void WithBasicConfiguration(HostBuilderContext hosting, IConfigurationBuilder config) { config.SetBasePath(hosting.HostingEnvironment.ContentRootPath); config.AddOcelot(ocelotConfigFileName, false, false); } public void WithBasicConfiguration(WebHostBuilderContext hosting, IConfigurationBuilder config) { config.SetBasePath(hosting.HostingEnvironment.ContentRootPath); config.AddOcelot(ocelotConfigFileName, false, false); } public static void WithAddOcelot(IServiceCollection services) => services.AddOcelot(); public static void WithUseOcelot(IApplicationBuilder app) => WithUseOcelotAsync(app).GetAwaiter().GetResult(); public static Task WithUseOcelotAsync(IApplicationBuilder app) => app.UseOcelot(); public int GivenOcelotIsRunning() => GivenOcelotIsRunning(null, null, null, null, null, null, null); public Task GivenOcelotIsRunningAsync() => GivenOcelotIsRunningAsync(null, null, null, null, null, null, null); public int GivenOcelotIsRunning(Action configureDelegate) => GivenOcelotIsRunning(configureDelegate, null, null, null, null, null, null); public Task GivenOcelotIsRunningAsync(Action configureDelegate) => GivenOcelotIsRunningAsync(configureDelegate, null, null, null, null, null, null); public int GivenOcelotIsRunning(Action configureServices) => GivenOcelotIsRunning(null, configureServices, null, null, null, null, null); public Task GivenOcelotIsRunningAsync(Action configureServices) => GivenOcelotIsRunningAsync(null, configureServices, null, null, null, null, null); public int GivenOcelotIsRunning(Action configureDelegate, Action configureServices) => GivenOcelotIsRunning(configureDelegate, configureServices, null, null, null, null, null); public Task GivenOcelotIsRunningAsync(Action configureDelegate, Action configureServices) => GivenOcelotIsRunningAsync(configureDelegate, configureServices, null, null, null, null, null); public int GivenOcelotIsRunning(Action? configureApp) => GivenOcelotIsRunning(null, null, configureApp, null, null, null, null); public Task GivenOcelotIsRunningAsync(Action? configureApp) => GivenOcelotIsRunningAsync(null, null, configureApp, null, null, null, null); public int GivenOcelotIsRunning(Action configureDelegate, Action configureServices, Action? configureApp) => GivenOcelotIsRunning(configureDelegate, configureServices, configureApp, null, null, null, null); public Task GivenOcelotIsRunningAsync(Action configureDelegate, Action configureServices, Action? configureApp) => GivenOcelotIsRunningAsync(configureDelegate, configureServices, configureApp, null, null, null, null); protected int GivenOcelotIsRunning( Action? configureDelegate, Action? configureServices, Action? configureApp, Action? configureWebHost, Action? postConfigureHost, Action? configureServer, Action? configureClient) { #if NET10_0_OR_GREATER return GivenOcelotHostIsRunning(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient) .GetAwaiter().GetResult(); #else return GivenOcelotIsRunningInternal(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient); #endif } protected Task GivenOcelotIsRunningAsync( Action? configureDelegate, Action? configureServices, Action? configureApp, Action? configureWebHost, Action? postConfigureHost, Action? configureServer, Action? configureClient) { #if NET10_0_OR_GREATER return GivenOcelotHostIsRunning(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient); #else return Task.Run(() => GivenOcelotIsRunningInternal(configureDelegate, configureServices, configureApp, configureWebHost, postConfigureHost, configureServer, configureClient)); #endif } #if (NET8_0 || NET9_0) private int GivenOcelotIsRunningInternal( Action? configureDelegate, Action? configureServices, Action? configureApp, Action? сonfigureWebHost, Action? postConfigureHost, Action? configureServer, Action? configureClient) { int port = PortFinder.GetRandomPort(); var baseUrl = DownstreamUrl(port); var builder = TestHostBuilder.Create(); if (сonfigureWebHost is not null) сonfigureWebHost(builder); else builder .ConfigureAppConfiguration(configureDelegate ?? WithBasicConfiguration) .ConfigureServices(configureServices ?? WithAddOcelot) .Configure(configureApp ?? WithUseOcelot) .UseUrls(baseUrl); // run Ocelot on specific port, rather than on std 80 port of TestServer postConfigureHost?.Invoke(builder); ocelotServer = new(builder) { BaseAddress = new(baseUrl) // will create Oc client with this base address, including port }; configureServer?.Invoke(ocelotServer); ocelotClient = ocelotServer.CreateClient(); configureClient?.Invoke(ocelotClient); return port; } #endif private static void SetBaseUrl(FileConfiguration configuration, string baseUrl) { configuration.GlobalConfiguration.BaseUrl = baseUrl; } protected async Task GivenOcelotHostIsRunning( Action? configureDelegate, Action? configureServices, Action? configureApp, Action? сonfigureWebHost, Action? postConfigureHost, Action? configureServer, Action? configureClient) { int port = PortFinder.GetRandomPort(); var baseUrl = DownstreamUrl(port); void ConfigureWeb(IWebHostBuilder builder) { builder .UseKestrel() .ConfigureAppConfiguration(configureDelegate ?? WithBasicConfiguration) .ConfigureServices(configureServices ?? WithAddOcelot) .Configure(configureApp ?? WithUseOcelot) .UseUrls(baseUrl); //.UseTestServer(o => o.BaseAddress = new(baseUrl)); postConfigureHost?.Invoke(builder); } var host = TestHostBuilder .CreateHost() .ConfigureWebHost(сonfigureWebHost ?? ConfigureWeb) .Build(); await host.StartAsync(); ocelotHost = host; //ocelotServer = host.GetTestServer(); //configureServer?.Invoke(ocelotServer!); ocelotClient = ocelotServer?.CreateClient(); ocelotClient ??= new() { BaseAddress = new(baseUrl), }; configureClient?.Invoke(ocelotClient); return port; } protected IServiceProvider OcelotServices { get => ocelotServer?.Services ?? ocelotHost!.Services; } #endregion #region GivenThereIsAServiceRunningOn public virtual void GivenThereIsAServiceRunningOn(int port, [CallerMemberName] string responseBody = "") => GivenThereIsAServiceRunningOn(port, HttpStatusCode.OK, responseBody); protected virtual HttpStatusCode MapStatus_StatusCode { get; set; } = HttpStatusCode.OK; protected virtual Func? MapStatus_ResponseBody { get; set; } protected virtual Task MapStatus(HttpContext context) { context.Response.StatusCode = (int)MapStatus_StatusCode; return context.Response.WriteAsync(MapStatus_ResponseBody?.Invoke(context) ?? string.Empty); } public virtual void GivenThereIsAServiceRunningOn(int port, HttpStatusCode statusCode, [CallerMemberName] string responseBody = "") { MapStatus_StatusCode = statusCode; MapStatus_ResponseBody ??= (ctx) => responseBody; handler.GivenThereIsAServiceRunningOn(port, MapStatus); } protected virtual Task MapOK(HttpContext context) { context.Response.StatusCode = StatusCodes.Status200OK; return context.Response.WriteAsync(MapStatus_ResponseBody?.Invoke(context) ?? string.Empty); } public virtual void GivenThereIsAServiceRunningOnPath(int port, string basePath, [CallerMemberName] string responseBody = "") { MapStatus_ResponseBody ??= (ctx) => responseBody; handler.GivenThereIsAServiceRunningOn(port, basePath, MapOK); } public virtual void GivenThereIsAServiceRunningOn(int port, string basePath, RequestDelegate requestDelegate) { handler.GivenThereIsAServiceRunningOn(port, basePath, requestDelegate); } #endregion public static void GivenIWait(int wait) => Thread.Sleep(wait); public static Task GivenIWaitAsync(int wait) => Task.Delay(wait); #region Cookies public void GivenIAddCookieToMyRequest(string cookie) => ocelotClient.ShouldNotBeNull().DefaultRequestHeaders.Add("Set-Cookie", cookie); public async Task WhenIGetUrlOnTheApiGatewayWithCookie(string url, string cookie, string value) => response = await WhenIGetUrlOnTheApiGateway(url, cookie, value); public async Task WhenIGetUrlOnTheApiGatewayWithCookie(string url, CookieHeaderValue cookie) => response = await WhenIGetUrlOnTheApiGateway(url, cookie); public Task WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) => WhenIGetUrlOnTheApiGateway(url, new CookieHeaderValue(cookie, value)); public Task WhenIGetUrlOnTheApiGateway(string url, CookieHeaderValue cookie) { var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); requestMessage.Headers.Add("Cookie", cookie.ToString()); return ocelotClient.ShouldNotBeNull().SendAsync(requestMessage); } #endregion #region Headers public void ThenTheResponseHeaderIs(string key, string value) { ThenTheResponseHeaderExists(key); var header = response?.Headers.GetValues(key) ?? []; header.Any(string.IsNullOrEmpty).ShouldBeFalse(); string.Join(';', header).ShouldBe(value); } public void ThenTheResponseContentHeaderIs(string key, string value) { ThenTheResponseContentHeaderExists(key); var header = response?.Content.Headers.GetValues(key) ?? []; header.Any(string.IsNullOrEmpty).ShouldBeFalse(); string.Join(';', header).ShouldBe(value); } public string ThenTheResponseHeaderExists(string key) { response.ShouldNotBeNull().Headers.Contains(key).ShouldBeTrue(); var header = response.Headers.GetValues(key); return string.Join(';', header); } public void ThenTheResponseHeaderExists(string key, bool exists) => response.ShouldNotBeNull().Headers.Contains(key).ShouldBe(exists); public string ThenTheResponseContentHeaderExists(string key) { response.ShouldNotBeNull().Content.Headers.Contains(key).ShouldBeTrue(); var header = response.Content.Headers.GetValues(key); return string.Join(';', header); } public void ThenTheResponseContentHeaderExists(string key, bool exists) => response.ShouldNotBeNull().Content.Headers.Contains(key).ShouldBe(exists); #endregion public void ThenTheResponseReasonPhraseIs(string expected) => response.ShouldNotBeNull().ReasonPhrase.ShouldBe(expected); public void GivenIHaveAddedATokenToMyRequest(string token, string scheme = "Bearer") { ArgumentNullException.ThrowIfNull(ocelotClient); ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(scheme, token); } public async Task WhenIGetUrlOnTheApiGateway(string url) => response = await ocelotClient.ShouldNotBeNull().GetAsync(url); public Task WhenIGetUrl(string url) => ocelotClient.ShouldNotBeNull().GetAsync(url); public async Task WhenIGetUrlOnTheApiGatewayWithBody(string url, string body) { var request = new HttpRequestMessage(HttpMethod.Get, url) { Content = new StringContent(body), }; response = await ocelotClient.ShouldNotBeNull().SendAsync(request); } public async Task WhenIGetUrlOnTheApiGatewayWithForm(string url, string name, IEnumerable> values) { var content = new MultipartFormDataContent(); var dataContent = new FormUrlEncodedContent(values); content.Add(dataContent, name); content.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); var request = new HttpRequestMessage(HttpMethod.Get, url) { Content = content, }; ArgumentNullException.ThrowIfNull(ocelotClient); response = await ocelotClient.SendAsync(request); } public async Task WhenIGetUrlOnTheApiGateway(string url, HttpContent content) { ArgumentNullException.ThrowIfNull(ocelotClient); var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; response = await ocelotClient.SendAsync(httpRequestMessage); } public async Task WhenIPostUrlOnTheApiGateway(string url, HttpContent content) { ArgumentNullException.ThrowIfNull(ocelotClient); var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; response = await ocelotClient.SendAsync(request); } public async Task WhenIPostUrlOnTheApiGateway(string url, string content) { ArgumentNullException.ThrowIfNull(ocelotClient); var postContent = new StringContent(content); response = await ocelotClient.PostAsync(url, postContent); } public async Task WhenIPostUrlOnTheApiGateway(string url, string content, string contentType) { ArgumentNullException.ThrowIfNull(ocelotClient); var postContent = new StringContent(content, new MediaTypeHeaderValue(contentType)); response = await ocelotClient.PostAsync(url, postContent); } public async Task WhenIDeleteUrlOnTheApiGateway(string url) => response = await ocelotClient.ShouldNotBeNull().DeleteAsync(url); public void GivenIAddAHeader(string key, string value) { key.ShouldNotBeNullOrEmpty(); value.ShouldNotBeNullOrEmpty(); ocelotClient.ShouldNotBeNull().DefaultRequestHeaders.TryAddWithoutValidation(key, value); } public static void WhenIDoActionMultipleTimes(int times, Action action) { for (int i = 0; i < times; i++) action?.Invoke(i); } public static async Task WhenIDoActionMultipleTimes(int times, Func action) { for (int i = 0; i < times; i++) await action.Invoke(i); } public static async Task WhenIDoActionForTime(TimeSpan time, Func action) { var watcher = Stopwatch.StartNew(); for (int i = 0; watcher.Elapsed < time; i++) { await action.Invoke(i); } watcher.Stop(); } public void ThenTheResponseBody([CallerMemberName] string testName = "") => ThenTheResponseBodyShouldBe(testName); public Task ThenTheResponseBodyAsync([CallerMemberName] string testName = "") => ThenTheResponseBodyShouldBeAsync(testName); public void ThenTheResponseBodyShouldBe(string expectedBody) => response.ShouldNotBeNull() .Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody); public Task ThenTheResponseBodyShouldBeAsync(string expectedBody) => response.ShouldNotBeNull() .Content.ReadAsStringAsync() .ContinueWith(t => t.Result.ShouldBe(expectedBody)); public void ThenTheResponseBodyShouldBe(string expectedBody, string customMessage) => response.ShouldNotBeNull() .Content.ReadAsStringAsync().GetAwaiter().GetResult().ShouldBe(expectedBody, customMessage); public Task ThenTheResponseBodyShouldBeAsync(string expectedBody, string customMessage) => response.ShouldNotBeNull() .Content.ReadAsStringAsync() .ContinueWith(t => t.Result.ShouldBe(expectedBody, customMessage)); public Task ThenTheResponseShouldBeAsync(HttpStatusCode expected, [CallerMemberName] string? expectedBody = null) { ThenTheStatusCodeShouldBe(expected); return ThenTheResponseBodyShouldBeAsync(expectedBody ?? Body(expectedBody)); } public Task ThenTheResponseBodyShouldBeEmpty() => ThenTheResponseBodyShouldBeAsync(string.Empty); public void ThenTheContentLengthIs(int expected) => response.ShouldNotBeNull().Content.Headers.ContentLength.ShouldBe(expected); public void ThenTheStatusCodeShouldBeOK() => ThenTheStatusCodeShouldBe(HttpStatusCode.OK); public void ThenTheStatusCodeShouldBe(HttpStatusCode expected) => response.ShouldNotBeNull().StatusCode.ShouldBe(expected); public void ThenTheStatusCodeShouldBe(int expected) => ((int)response.ShouldNotBeNull().StatusCode).ShouldBe(expected); public Task ReleasePortAsync(params int[] ports) => handler.ReleasePortAsync(ports); #region Dispose pattern /// /// Public implementation of Dispose pattern callable by consumers. /// public virtual void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool _disposedValue; /// Protected implementation of Dispose pattern. /// Flag to trigger actual disposing operation. protected virtual void Dispose(bool disposing) { if (_disposedValue) { return; } if (disposing) { ocelotClient?.Dispose(); ocelotServer?.Dispose(); ocelotHost?.Dispose(); response?.Dispose(); handler.Dispose(); DeleteFiles(); DeleteFolders(); } _disposedValue = true; } protected virtual void DeleteFiles() { foreach (var file in Files) { if (!File.Exists(file)) continue; try { File.Delete(file); } catch (Exception e) { Console.WriteLine(e); } } Files.Clear(); } protected virtual void DeleteFolders() { foreach (var folder in Folders) { try { var f = new DirectoryInfo(folder); if (f.Exists && f.FullName != AppContext.BaseDirectory) { f.Delete(true); } } catch (Exception e) { Console.WriteLine(e); } } Folders.Clear(); } #endregion } ================================================ FILE: testing/Authentication/AuthenticationSteps.cs ================================================ using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Ocelot.Authorization; using Ocelot.Configuration.File; using Ocelot.DependencyInjection; using Shouldly; using System.Data; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Json; using System.Runtime.CompilerServices; using System.Security.Claims; using System.Text; using System.Text.Json; namespace Ocelot.Testing.Authentication; public class AuthenticationSteps : AcceptanceSteps { protected BearerToken? token; private readonly Dictionary _jwtSigningServers; public string JwtSigningServerUrl { get => _jwtSigningServers.First().Key; } public AuthenticationSteps() : base() { _jwtSigningServers = []; } public override void Dispose() { foreach (var kv in _jwtSigningServers) { IDisposable server = _jwtSigningServers[kv.Key]; server?.Dispose(); } _jwtSigningServers.Clear(); base.Dispose(); GC.SuppressFinalize(this); } protected void WithThreemammalsOptions(JwtBearerOptions o) { o.Audience = AuthToken.Audience; // "threemammals.com"; o.Authority = new Uri(JwtSigningServerUrl).Authority; o.RequireHttpsMetadata = false; o.TokenValidationParameters = new() { ValidateIssuer = true, ValidIssuer = new Uri(JwtSigningServerUrl).Authority, ValidateAudience = true, ValidAudience = ocelotClient?.BaseAddress?.Authority, ValidateIssuerSigningKey = true, IssuerSigningKey = AuthToken.IssuerSigningKey(), }; } public void WithJwtBearerAuthentication(IServiceCollection services) => WithJwtBearerAuthentication(services, true); public void WithJwtBearerAuthentication(IServiceCollection services, bool addOcelot) { if (addOcelot) services.AddOcelot(); services.AddAuthentication().AddJwtBearer(WithThreemammalsOptions); } public static /*IHost*/ WebApplication CreateJwtSigningServer(string url, string[] apiScopes) { apiScopes ??= [OcelotScopes.Api]; var builder = TestWebBuilder.CreateSlimBuilder(); builder.WebHost.UseUrls(url); builder.Services .AddLogging() .AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = "threemammals.com", // see mycert2.pfx ValidAudience = "threemammals.com", IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Ocelot.AcceptanceTests.Authentication")), }; }); var app = builder.Build(); app.MapGet("/connect", () => "Hello! Connected!"); app.MapPost("/token", (AuthenticationTokenRequest model) => { // The signing server should be eligible to sign predefined claims as specified in its configuration. // If an unknown scope or claim is requested for inclusion in a JWT, the server should reject the request. // Therefore, the server configuration should be well-known to the client; otherwise, it poses a security risk. if (!apiScopes.Intersect(model.Scopes?.Split(' ') ?? []).Any()) { return Results.BadRequest(); } var token = GenerateToken(url, model); return Results.Json(token); }); return app; } protected static async Task VerifyJwtSigningServerStarted(string url, CancellationToken token, HttpClient? client = null) { client ??= new HttpClient(); var response = await client.GetAsync($"{url}/connect", token); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(token); json.ShouldNotBeNullOrEmpty(); } public Task GivenThereIsExternalJwtSigningService(string[] extraScopes, CancellationToken token) { List scopes = [OcelotScopes.Api, OcelotScopes.Api2]; scopes.AddRange(extraScopes); var url = DownstreamUrl(PortFinder.GetRandomPort()); var server = CreateJwtSigningServer(url, [.. scopes]); _jwtSigningServers.Add(url, server); return server.StartAsync(token) .ContinueWith(t => VerifyJwtSigningServerStarted(url, token), token) .ContinueWith(t => url, token); } public void GivenIHaveAddedATokenToMyRequest() => GivenIHaveAddedATokenToMyRequest(token); public void GivenIHaveAddedATokenToMyRequest(BearerToken? token) => GivenIHaveAddedATokenToMyRequest(token?.AccessToken ?? string.Empty, JwtBearerDefaults.AuthenticationScheme); public AuthenticationTokenRequest GivenAuthTokenRequest(string scope, IEnumerable>? claims = null, [CallerMemberName] string testName = "") { var auth = new AuthenticationTokenRequest() { Audience = ocelotClient?.BaseAddress?.Authority ?? string.Empty, // Ocelot DNS is token audience ApiSecret = testName, // "secret", Scopes = scope ?? OcelotScopes.Api, Claims = claims is null ? new() : new(claims), UserId = testName, UserName = testName, }; return auth; } public Task GivenIHaveAToken([CallerMemberName] string testName = "") => GivenIHaveAToken(OcelotScopes.Api, null, JwtSigningServerUrl, null, testName); public async Task GivenIHaveAToken(string scope, IEnumerable>? claims = null, string? issuerUrl = null, string? audience = null, [CallerMemberName] string testName = "") { var auth = GivenAuthTokenRequest(scope, claims, testName); auth.Audience = audience ?? ocelotClient?.BaseAddress?.Authority ?? string.Empty; return token = await GivenToken(auth, string.Empty, issuerUrl); } public async Task GivenIHaveATokenWithUrlPath(string path, string scope, [CallerMemberName] string testName = "") { var auth = GivenAuthTokenRequest(scope, null!, testName); return token = await GivenToken(auth, path); } public readonly Dictionary AuthTokens = []; public AuthenticationTokenRequest AuthToken => AuthTokens.Count > 0 ? AuthTokens.First().Value : new(); public event EventHandler? AuthTokenRequesting; protected virtual void OnAuthenticationTokenRequest(AuthenticationTokenRequestEventArgs e) => AuthTokenRequesting?.Invoke(this, e); protected async Task GivenToken(AuthenticationTokenRequest auth, string path = "", string? issuerUrl = null) { using var http = new HttpClient(); issuerUrl ??= JwtSigningServerUrl; AuthTokens[issuerUrl] = auth; OnAuthenticationTokenRequest(new(auth)); var tokenUrl = $"{issuerUrl + path}/token"; var content = JsonContent.Create(auth); var response = await http.PostAsync(tokenUrl, content); var responseContent = await response.Content.ReadAsStringAsync(); response.EnsureSuccessStatusCode(); return JsonSerializer.Deserialize(responseContent, JsonSerializerOptions.Web); } protected FileRoute GivenAuthRoute(int port, string path, FileAuthenticationOptions options) { FileRoute? r = GivenRoute(port, path, path) as FileRoute; r!.AuthenticationOptions = options; return r; } public FileRoute GivenAuthRoute(int port, string scheme = JwtBearerDefaults.AuthenticationScheme, bool allowAnonymous = false, string[]? scopes = null, string? method = null) { // FileRoute r = GivenDefaultRoute(port)?.WithMethods(method ?? HttpMethods.Get) as FileRoute; FileRoute r = GivenDefaultRoute(port); r.UpstreamHttpMethod.Add(method ?? HttpMethods.Get); r.AuthenticationOptions = new(scheme) { AllowAnonymous = allowAnonymous, AllowedScopes = scopes?.ToList(), }; return r; } public static FileGlobalConfiguration GivenGlobalAuthConfiguration( string scheme = JwtBearerDefaults.AuthenticationScheme, string[]? allowedScopes = null) => new() { AuthenticationOptions = new() { AllowedScopes = [.. allowedScopes ?? []], AuthenticationProviderKeys = [scheme], }, }; //private IConfiguration _config; private readonly UserManager? _userManager = default; public async Task GenerateTokenAsync(IdentityUser user, string issuer, string audience, string secretKey) { var userClaims = await _userManager?.GetClaimsAsync(user)!; var roles = await _userManager.GetRolesAsync(user); var roleClaims = roles .Select(role => new Claim(ClaimTypes.Role, role)); var claims = new List { new(JwtRegisteredClaimNames.Sub, user.Id), new(JwtRegisteredClaimNames.Email, user.Email ?? string.Empty), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), } .Union(userClaims) .Union(roleClaims); var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var expiry = DateTime.UtcNow.AddMinutes(1); var token = new JwtSecurityToken( issuer: issuer, //_config["Jwt:Issuer"], audience: audience, // _config["Jwt:Audience"], claims: claims, expires: expiry, signingCredentials: creds ); var jwt = new JwtSecurityTokenHandler().WriteToken(token); BearerToken bt = new() { AccessToken = jwt, ExpiresIn = (int)(expiry - DateTime.UtcNow).TotalSeconds, TokenType = JwtBearerDefaults.AuthenticationScheme, }; return bt; } private static bool IsRoleKey(KeyValuePair kv) => nameof(ClaimTypes.Role).Equals(kv.Key, StringComparison.OrdinalIgnoreCase) || ClaimTypes.Role.Equals(kv.Key); private static bool IsNotRoleKey(KeyValuePair kv) => !IsRoleKey(kv); public static BearerToken GenerateToken(string issuerUrl, AuthenticationTokenRequest auth) { if (auth is null) return new(); var userClaims = auth.Claims // await _userManager.GetClaimsAsync(user); .Where(IsNotRoleKey) .Select(kv => new Claim(kv.Key, kv.Value)) .ToList(); var roleClaims = auth.Claims // await _userManager.GetRolesAsync(user); .Where(IsRoleKey) .Select(kv => new Claim(/*ClaimTypes.Role*/kv.Key, kv.Value)) // ClaimTypes.Role is not supported, see AuthorizationTests.Should_fix_issue_240 .ToList(); var claims = new List(4 + auth.Claims.Count) { new(JwtRegisteredClaimNames.Sub, auth.UserId ?? string.Empty), new(OcelotClaims.OcSub, auth.UserId ?? string.Empty), // this is a handy lifehack to fix current authorization services like IScopesAuthorizer and IClaimsAuthorizer, which don't support JWT standard and claim types in URL form, aka the ':' delimiter issue with the JSON configuration provider new(JwtRegisteredClaimNames.Email, $"{auth.UserName}@ocelot.net"), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new(ScopesAuthorizer.Scope, auth.Scopes ?? string.Empty), }; claims.AddRange(roleClaims); claims.AddRange(userClaims); var credentials = new SigningCredentials(auth.IssuerSigningKey(), SecurityAlgorithms.HmacSha256); var expiry = DateTime.UtcNow.AddMinutes(1); var token = new JwtSecurityToken( issuer: new Uri(issuerUrl).Authority, // URL http://localhost:1234 -> DNS localhost:1234 //_config["Jwt:Issuer"], audience: auth.Audience, // _config["Jwt:Audience"], claims: claims, expires: expiry, signingCredentials: credentials ); var jwt = string.Empty; try { jwt = new JwtSecurityTokenHandler().WriteToken(token); } catch (Exception ex) { jwt = ex.Message; } BearerToken bt = new() { AccessToken = jwt, ExpiresIn = (int)(expiry - DateTime.UtcNow).TotalSeconds, TokenType = JwtBearerDefaults.AuthenticationScheme, }; return bt; } public static FileAuthenticationOptions GivenOptions(bool? allowAnonymous = null, List? allowedScopes = null, string[]? schemes = null) => new() { AllowAnonymous = allowAnonymous, AllowedScopes = allowedScopes, AuthenticationProviderKeys = schemes, }; } ================================================ FILE: testing/Authentication/AuthenticationTokenRequest.cs ================================================ using Microsoft.IdentityModel.Tokens; using Ocelot.Infrastructure.Extensions; using System.Text; using System.Text.Json.Serialization; namespace Ocelot.Testing.Authentication; public class AuthenticationTokenRequest { [JsonInclude] public string? Audience { get; set; } [JsonInclude] public string? UserId { get; set; } [JsonInclude] public string? UserName { get; set; } [JsonInclude] public string ApiSecret { get => _apiSecret; set { _apiSecret = value; _issuerSigningKey = null; } } [JsonInclude] public string? Scopes { get; set; } [JsonInclude] public List> Claims { get; set; } = []; private SymmetricSecurityKey? _issuerSigningKey; private string _apiSecret = string.Empty; public SymmetricSecurityKey? IssuerSigningKey() { if (_issuerSigningKey != null) return _issuerSigningKey; if (_apiSecret.IsEmpty()) return _issuerSigningKey = null; // System.ArgumentOutOfRangeException: 'IDX10720: Unable to create KeyedHashAlgorithm for algorithm 'HS256', the key size must be greater than: '256' bits, key has '160' bits. (Parameter 'keyBytes')' // Make sure the security key is at least 32 characters long, // So, multiply the password body by repeating it. int size = 256 / 8, length = _apiSecret.Length; var securityKey = length >= size ? _apiSecret : string.Join('|', Enumerable.Repeat(_apiSecret, size / length)) + _apiSecret[..(size % length)]; // total length should be 32 chars return _issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)); } } ================================================ FILE: testing/Authentication/AuthenticationTokenRequestEventArgs.cs ================================================ namespace Ocelot.Testing.Authentication; public class AuthenticationTokenRequestEventArgs(AuthenticationTokenRequest request) : EventArgs { public AuthenticationTokenRequest Request { get; } = request; } ================================================ FILE: testing/Authentication/OcelotScopes.cs ================================================ namespace Ocelot.Testing.Authentication; public static class OcelotScopes { public const string Api = "api"; public const string Api2 = "api2"; public const string OcAdmin = "oc-admin"; } public static class OcelotClaims { public const string OcSub = "oc-sub"; } ================================================ FILE: testing/BearerToken.cs ================================================ using Newtonsoft.Json; using System.Text.Json.Serialization; namespace Ocelot.Testing; public class BearerToken { [JsonInclude] [JsonProperty("access_token")] public string? AccessToken { get; set; } [JsonInclude] [JsonProperty("expires_in")] public int ExpiresIn { get; set; } [JsonInclude] [JsonProperty("token_type")] public string? TokenType { get; set; } } ================================================ FILE: testing/Boxing/Box.cs ================================================ namespace Ocelot.Testing.Boxing; public class Box { public static TResult In(TBoxee instance) where TBoxee : class where TResult : Box => (TResult)Activator.CreateInstance(typeof(TResult), instance)!; public static TResult With(TBoxee instance) where TBoxee : class where TResult : Box => In(instance); } public class Box : Box { protected readonly T Instance; protected readonly Type Me; public Box(T instance, string? typeName = null) { if (instance?.GetType().FullName != typeName) throw new ArgumentException($"Is not type of {typeName ?? "?"}", nameof(instance)); Instance = instance; Me = typeof(T); } public T Out() => Instance; public T Unbox() => Instance; } ================================================ FILE: testing/Boxing/FileRouteBox.cs ================================================ using Ocelot.Configuration.File; using System.Collections; namespace Ocelot.Testing.Boxing; public static class FileRouteBox { public static FileRouteBox In(T route) where T : FileRoute => new(route); public static FileRouteBox With(T route) where T : FileRoute => new(route); } public class FileRouteBox(T route) : Box(route, "Ocelot.Configuration.File.FileRoute") where T : FileRoute { public static FileRouteBox In(T route) => new(route); public static FileRouteBox With(T route) => new(route); public FileRouteBox Hosts(params H[] hosts) where H : class // FileHostAndPort { //route.DownstreamHostAndPorts.AddRange(hosts); var property = Me.GetProperty("DownstreamHostAndPorts"); IList? downstreamHostAndPorts = property?.GetValue(Instance) as IList; ArgumentNullException.ThrowIfNull(downstreamHostAndPorts); for (int i = 0; i < hosts.Length; i++) { var host = hosts[i]; if (host?.GetType().FullName != "Ocelot.Configuration.File.FileHostAndPort") throw new ArgumentException($"Argument at index {i} is not type of Ocelot.Configuration.File.FileHostAndPort", nameof(hosts)); downstreamHostAndPorts.Add(host); } return this; } public FileRouteBox Priority(int priority) { //route.Priority = priority; var property = Me.GetProperty("Priority"); property?.SetValue(Instance, priority); return this; } public FileRouteBox Methods(params string[] methods) { //route.UpstreamHttpMethod = [.. methods]; var property = Me.GetProperty("UpstreamHttpMethod") ?? throw new NullReferenceException("UpstreamHttpMethod"); var collection = Activator.CreateInstance(property.PropertyType, (IEnumerable)methods); property.SetValue(Instance, collection); return this; } public FileRouteBox UpstreamHeaderTransform(params KeyValuePair[] pairs) { //route.UpstreamHeaderTransform = new Dictionary(pairs); var property = Me.GetProperty("UpstreamHeaderTransform"); IDictionary upstreamHeaderTransform = property?.GetValue(Instance) as IDictionary ?? throw new ArgumentNullException(nameof(upstreamHeaderTransform)); for (int i = 0; i < pairs.Length; i++) { var kv = pairs[i]; upstreamHeaderTransform.Add(kv.Key, kv.Value); } return this; } public FileRouteBox UpstreamHeaderTransform(string key, string value) { //route.UpstreamHeaderTransform.TryAdd(key, Instance); var property = Me.GetProperty("UpstreamHeaderTransform"); IDictionary upstreamHeaderTransform = property?.GetValue(Instance) as IDictionary ?? throw new ArgumentNullException(nameof(upstreamHeaderTransform)); upstreamHeaderTransform.TryAdd(key, value); return this; } public FileRouteBox DownstreamHeaderTransform(string key, string value) { //route.DownstreamHeaderTransform.TryAdd(key, value); var property = Me.GetProperty("DownstreamHeaderTransform"); IDictionary downstreamHeaderTransform = property?.GetValue(Instance) as IDictionary ?? throw new ArgumentNullException(nameof(downstreamHeaderTransform)); downstreamHeaderTransform.TryAdd(key, value); return this; } public FileRouteBox HandlerOptions(O options) where O : class // FileHttpHandlerOptions { //route.HttpHandlerOptions = options; if (options?.GetType().FullName != "Ocelot.Configuration.File.FileHttpHandlerOptions") throw new ArgumentException($"Is not type of Ocelot.Configuration.File.FileHttpHandlerOptions", nameof(options)); var property = Me.GetProperty("HttpHandlerOptions"); property?.SetValue(Instance, options); return this; } public FileRouteBox Key(string? key) { //route.Key = key; var property = Me.GetProperty("Key"); property?.SetValue(Instance, key); return this; } public FileRouteBox UpstreamHost(string? upstreamHost) { //route.UpstreamHost = upstreamHost; var property = Me.GetProperty("UpstreamHost"); property?.SetValue(Instance, upstreamHost); return this; } } ================================================ FILE: testing/Boxing/FileRouteExtensions.cs ================================================ using Ocelot.Configuration.File; namespace Ocelot.Testing.Boxing; /// /// For type: /// public static class FileRouteExtensions { public static R WithHosts(this R route, params H[] hosts) where R : FileRoute where H : FileHostAndPort => Box.In, R>(route).Hosts(hosts).Out(); //route.DownstreamHostAndPorts.AddRange(hosts); public static R WithPriority(this R route, int priority) where R : FileRoute => FileRouteBox.In(route).Priority(priority).Out(); //route.Priority = priority; public static R WithMethods(this R route, params string[] methods) where R : FileRoute => FileRouteBox.In(route).Methods(methods).Out(); //route.UpstreamHttpMethod = [.. methods]; public static R WithUpstreamHeaderTransform(this R route, params KeyValuePair[] pairs) where R : FileRoute => new FileRouteBox(route).UpstreamHeaderTransform(pairs).Out(); //route.UpstreamHeaderTransform = new Dictionary(pairs); public static R WithUpstreamHeaderTransform(this R route, string key, string value) where R : FileRoute => new FileRouteBox(route).UpstreamHeaderTransform(key, value).Out(); //route.UpstreamHeaderTransform.TryAdd(key, value); public static R WithDownstreamHeaderTransform(this R route, string key, string value) where R : FileRoute => new FileRouteBox(route).DownstreamHeaderTransform(key, value).Out(); //route.DownstreamHeaderTransform.TryAdd(key, value); public static R WithHttpHandlerOptions(this R route, O options) where R : FileRoute where O : FileHttpHandlerOptions => new FileRouteBox(route).HandlerOptions(options).Out(); //route.HttpHandlerOptions = options; public static R WithKey(this R route, string? key) where R : FileRoute => new FileRouteBox(route).Key(key).Out(); //route.Key = key; public static R WithUpstreamHost(this R route, string? upstreamHost) where R : FileRoute => new FileRouteBox(route).UpstreamHost(upstreamHost).Out(); //route.UpstreamHost = upstreamHost; } ================================================ FILE: testing/FileUnit.cs ================================================ namespace Ocelot.Testing; public class FileUnit : Unit, IDisposable { protected string primaryConfigFileName; protected string globalConfigFileName; protected string environmentConfigFileName; protected readonly List files; protected readonly List folders; protected FileUnit() : this(null) { } protected FileUnit(string? folder) { folder ??= TestID; Directory.CreateDirectory(folder); folders = [folder]; primaryConfigFileName = Path.Combine(folder, /*ConfigurationBuilderExtensions.PrimaryConfigFile*/ "ocelot.json"); globalConfigFileName = Path.Combine(folder, /*ConfigurationBuilderExtensions.GlobalConfigFile*/ "ocelot.global.json"); environmentConfigFileName = Path.Combine(folder, string.Format(/*ConfigurationBuilderExtensions.EnvironmentConfigFile*/"ocelot.{0}.json", EnvironmentName())); files = [ primaryConfigFileName, globalConfigFileName, environmentConfigFileName, ]; } protected virtual string EnvironmentName() => TestID; public virtual void Dispose() { Dispose(true); GC.SuppressFinalize(this); } private bool _disposed; /// /// Protected implementation of Dispose pattern. /// /// Flag to trigger actual disposing operation. protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { DeleteFiles(); DeleteFolders(); } _disposed = true; } protected void DeleteFiles() { foreach (var file in files) { try { var f = new FileInfo(file); if (f.Exists) { f.Delete(); } } catch (Exception e) { Console.WriteLine(e); } } } protected void DeleteFolders() { foreach (var folder in folders) { try { var f = new DirectoryInfo(folder); if (f.Exists && f.FullName != AppContext.BaseDirectory) { f.Delete(true); } } catch (Exception e) { Console.WriteLine(e); } } } } ================================================ FILE: testing/Ocelot.Testing.csproj ================================================  net8.0;net9.0;net10.0 enable enable True True snupkg 25.0.0-beta.4 Ocelot.Testing A shared library for testing the Ocelot core library and its extension packages, including both acceptance and unit tests https://github.com/ThreeMammals/Ocelot/blob/main/ReleaseNotes.md ocelot_icon.png README.md LICENSE.md dotnet;ocelot;testing https://github.com/ThreeMammals/Ocelot https://github.com/ThreeMammals/Ocelot.git Tom Pallister, Raman Maksimchuk Three Mammals Ocelot Gateway © 2026 Three Mammals. MIT licensed OSS ..\codeanalysis.ruleset ================================================ FILE: testing/Ocelot.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace Ocelot.Testing; internal class Ocelot { private static Assembly? ocelotAssembly; public static Assembly OcelotAssembly { get => ocelotAssembly ??= Assembly.Load(new AssemblyName("Ocelot")); } public static object? CreateFileRoute(out Type type) { type = OcelotAssembly.GetType("Ocelot.Configuration.File.FileRoute")!; return Activator.CreateInstance(type); } public static object? CreateFileConfiguration(out Type type) { type = OcelotAssembly.GetType("Ocelot.Configuration.File.FileConfiguration")!; return Activator.CreateInstance(type); } public static object? CreateFileHostAndPort(string host, int port, out Type type) { type = OcelotAssembly.GetType("Ocelot.Configuration.File.FileHostAndPort")!; return Activator.CreateInstance(type, host, port); } public static object? AddOcelot(IServiceCollection services) { var type = OcelotAssembly.GetType("Ocelot.DependencyInjection.ServiceCollectionExtensions")!; var method = type.GetMethods(BindingFlags.Static | BindingFlags.Public) .FirstOrDefault(m => m.Name == "AddOcelot" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(IServiceCollection)); return method?.Invoke(null, [services]); } public static Task UseOcelot(IApplicationBuilder builder) { var type = OcelotAssembly.GetType("Ocelot.Middleware.OcelotMiddlewareExtensions")!; var method = type.GetMethods(BindingFlags.Static | BindingFlags.Public) .FirstOrDefault(m => m.Name == "UseOcelot" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == typeof(IApplicationBuilder)); return method?.Invoke(null, [builder]) as Task ?? Task.FromResult(builder); } } ================================================ FILE: testing/PortFinder.cs ================================================ using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; namespace Ocelot.Testing; public static class PortFinder { private const int EndPortRange = 45000; private static volatile int CurrentPort = 20000; private static readonly object SyncRoot = new(); //private static readonly ConcurrentBag UsedPorts = new(); /// /// Gets a pseudo-random port from the range [, ] for one testing scenario. /// /// New allocated port. /// Critical situation where available ports range has been exceeded. public static int GetRandomPort() { lock (SyncRoot) { ExceedingPortRangeException.ThrowIf(CurrentPort > EndPortRange); while (!TryUsePort(++CurrentPort)); return CurrentPort; } } /// /// Gets the exact number of ports from the range [, ] for one testing scenario. /// /// The number of wanted ports. /// Array of allocated ports. /// Critical situation where available ports range has been exceeded. public static int[] GetPorts(int count) { var ports = new int[count]; for (int i = 0; i < count; i++) { ports[i] = GetRandomPort(); } return ports; } private static bool TryUsePort(int port) { //UsedPorts.Add(port); // TODO Review or remove, now useless Socket? socket = null; try { var ipe = new IPEndPoint(IPAddress.Loopback, port); socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); socket.Bind(ipe); socket.Close(); return true; } catch { return false; } finally { socket?.Dispose(); } } } public class ExceedingPortRangeException : Exception { public ExceedingPortRangeException() : base("Cannot find available port to bind to!") { } public static void ThrowIf(bool condition) => _ = condition ? throw new ExceedingPortRangeException() : 0; } ================================================ FILE: testing/README.md ================================================ # Ocelot.Testing [![Release Package](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/release.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/release.yml) [![Publish Package](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/publish.yml/badge.svg)](https://github.com/ThreeMammals/Ocelot.Testing/actions/workflows/publish.yml) [![NuGet](https://img.shields.io/nuget/v/Ocelot.Testing?logo=nuget&label=NuGet&color=blue)](https://www.nuget.org/packages/Ocelot.Testing/ "Download Ocelot.Testing from NuGet.org") [![Downloads](https://img.shields.io/nuget/dt/Ocelot.Testing?logo=nuget&label=Downloads)](https://www.nuget.org/packages/Ocelot.Testing/ "Total Ocelot.Testing downloads from NuGet.org") A shared library for testing the [Ocelot](https://github.com/ThreeMammals/Ocelot) core library and its [extension packages](https://www.nuget.org/profiles/ThreeMammals), including both acceptance and unit tests. - **Core package**: [![Head package](https://img.shields.io/nuget/v/Ocelot?logo=nuget&label=Ocelot)](https://www.nuget.org/packages/Ocelot/ "Ocelot package") - **Core repository**: [![Head repository](https://img.shields.io/github/v/release/ThreeMammals/Ocelot?logo=github&label=Ocelot)](https://github.com/ThreeMammals/Ocelot/ "Ocelot repository") - **Extension packages:** [![Extension packages](https://img.shields.io/badge/NuGet-ThreeMammals-blue?logo=nuget)](https://www.nuget.org/profiles/ThreeMammals "Ocelot extension packages owned by ThreeMammals") ================================================ FILE: testing/ServiceHandler.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using System.Collections.Concurrent; using System.Net; namespace Ocelot.Testing; // TODO 1. Refactor in future to make this class a base class of acceptance steps // TODO 2. Develop async versions for each sync method public class ServiceHandler : IDisposable { #if NET10_0_OR_GREATER private readonly ConcurrentDictionary _hosts = new(); #else private readonly ConcurrentDictionary _hosts = new(); #endif public void Dispose() { foreach (var kv in _hosts) { kv.Value?.Dispose(); } _hosts.Clear(); GC.SuppressFinalize(this); } protected Task AddOrStopAsync( string key, #if NET10_0_OR_GREATER IHost host) #else IWebHost host) #endif { if (_hosts.TryAdd(key, host)) return Task.CompletedTask; var h = _hosts.GetOrAdd(key, host); _hosts[key] = host; // Shutdown old host return h.StopAsync().ContinueWith(t => h.Dispose(), TaskContinuationOptions.ExecuteSynchronously); } public Task ReleasePortAsync(params int[] ports) { List tasks = new(ports.Length); foreach (int port in ports) { var kv = _hosts.SingleOrDefault(x => new Uri(x.Key).Port == port); if (kv.Key is null || kv.Value is null) continue; var host = kv.Value; var task = host.StopAsync() .ContinueWith(t => { host.Dispose(); _hosts.TryRemove(kv); }); tasks.Add(task); } return Task.WhenAll(tasks); } #if NET10_0_OR_GREATER private static IHost CreateHost(Action configureWeb) #else private static IWebHost CreateHost(Action configureWeb) #endif { #if NET10_0_OR_GREATER var host = TestHostBuilder.CreateHost() .ConfigureWebHost(configureWeb) .Build(); #else var builder = TestHostBuilder.Create(); configureWeb(builder); var host = builder.Build(); #endif return host; } #if NET10_0_OR_GREATER public IHost #else public IWebHost #endif GivenThereIsAServiceRunningOn(string baseUrl, RequestDelegate handler) { void ConfigureWeb(IWebHostBuilder builder) => builder .UseUrls(baseUrl) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .Configure(app => app.Run(handler)); var host = CreateHost(ConfigureWeb); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); return host; } public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, RequestDelegate handler) { void ConfigureWeb(IWebHostBuilder builder) => builder .UseUrls(baseUrl) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .Configure(app => app.UsePathBase(basePath).Run(handler)); var host = CreateHost(ConfigureWeb); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); } public void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, Action configureServices, RequestDelegate handler) { void ConfigureWeb(IWebHostBuilder builder) => builder .UseUrls(baseUrl) .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureServices(configureServices) .Configure(app => app.UsePathBase(basePath).Run(handler)); var host = CreateHost(ConfigureWeb); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); } public void GivenThereIsAServiceRunningOnWithKestrelOptions(string baseUrl, string basePath, Action options, RequestDelegate handler) { void ConfigureWeb(IWebHostBuilder builder) => builder .UseUrls(baseUrl) .UseKestrel() .ConfigureKestrel(options ?? WithDefaultKestrelServerOptions) // ! .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .Configure(app => app.UsePathBase(basePath).Run(handler)); var host = CreateHost(ConfigureWeb); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); } internal void WithDefaultKestrelServerOptions(KestrelServerOptions options) { } public void GivenThereIsAHttpsServiceRunningOn(string baseUrl, string basePath, string fileName, string password, int port, RequestDelegate handler) { void WithKestrelOptions(KestrelServerOptions options) => options.Listen(IPAddress.Loopback, port, o => o.UseHttps(fileName, password)); void ConfigureWeb(IWebHostBuilder builder) => builder .UseUrls(baseUrl) .UseKestrel(WithKestrelOptions) .UseContentRoot(Directory.GetCurrentDirectory()) .Configure(app => app.UsePathBase(basePath).Run(handler)); var host = CreateHost(ConfigureWeb); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); } #region Advanced helpers public static string Localhost(int port) => $"{Uri.UriSchemeHttp}://localhost:{port}"; public void GivenThereIsAServiceRunningOn(int port, RequestDelegate handler) => GivenThereIsAServiceRunningOn(Localhost(port), handler); public void GivenThereIsAServiceRunningOn(int port, string path, RequestDelegate handler) => GivenThereIsAServiceRunningOn(Localhost(port), path, handler); #endregion #if NET10_0_OR_GREATER public IHost #else public IWebHost #endif GivenThereIsAServiceRunningOn(int port, Action? configureDelegate, Action? configureLogging, Action? configureServices, Action? configureApp, Action? configureWebHost) => GivenThereIsAServiceRunningOn(Localhost(port), configureDelegate, configureLogging, configureServices, configureApp, configureWebHost); #if NET10_0_OR_GREATER public IHost #else public IWebHost #endif GivenThereIsAServiceRunningOn(string baseUrl, Action? configureDelegate, Action? configureLogging, Action? configureServices, Action? configureApp, Action? configureWebHost) { void ConfigureWeb(IWebHostBuilder builder) { builder.UseUrls(baseUrl).UseKestrel(); if (configureDelegate != null) builder.ConfigureAppConfiguration(configureDelegate); if (configureLogging != null) builder.ConfigureLogging(configureLogging); if (configureServices != null) builder.ConfigureServices(configureServices); if (configureApp != null) builder.Configure(configureApp); configureWebHost?.Invoke(builder); } var host = CreateBuilder(ConfigureWeb).Build(); AddOrStopAsync(baseUrl, host).GetAwaiter().GetResult(); host.Start(); return host; } #if NET10_0_OR_GREATER public Task #else public Task #endif GivenThereIsAServiceRunningOnAsync(int port, Action? configureDelegate, Action? configureLogging, Action? configureServices, Action? configureApp, Action? configureWebHost) => GivenThereIsAServiceRunningOnAsync(Localhost(port), configureDelegate, configureLogging, configureServices, configureApp, configureWebHost); #if NET10_0_OR_GREATER public Task #else public Task #endif GivenThereIsAServiceRunningOnAsync(string baseUrl, Action? configureDelegate, Action? configureLogging, Action? configureServices, Action? configureApp, Action? configureWebHost) { void ConfigureWeb(IWebHostBuilder builder) { builder.UseUrls(baseUrl).UseKestrel(); if (configureDelegate != null) builder.ConfigureAppConfiguration(configureDelegate); if (configureLogging != null) builder.ConfigureLogging(configureLogging); if (configureServices != null) builder.ConfigureServices(configureServices); if (configureApp != null) builder.Configure(configureApp); configureWebHost?.Invoke(builder); } var host = CreateBuilder(ConfigureWeb).Build(); return AddOrStopAsync(baseUrl, host) .ContinueWith(t => host.StartAsync()) .ContinueWith(t => host, TaskContinuationOptions.ExecuteSynchronously); } #if NET10_0_OR_GREATER private static IHostBuilder #else private static IWebHostBuilder #endif CreateBuilder(Action configureWeb) { #if NET10_0_OR_GREATER return TestHostBuilder.CreateHost() .ConfigureWebHost(configureWeb); #else var builder = TestHostBuilder.Create(); configureWeb(builder); return builder; #endif } } ================================================ FILE: testing/StreamExtensions.cs ================================================ namespace Ocelot.Testing; public static class StreamExtensions { public static string AsString(this Stream stream) { using var reader = new StreamReader(stream); var text = reader.ReadToEnd(); return text; } } ================================================ FILE: testing/TestHostBuilder.cs ================================================ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Ocelot.Testing; public sealed class TestHostBuilder #if NET10_0_OR_GREATER : HostBuilder #else : WebHostBuilder #endif { #if NET10_0_OR_GREATER public static IHostBuilder Create() => new HostBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes); #else public static IWebHostBuilder Create() => new WebHostBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes); #endif #if NET10_0_OR_GREATER public static IHostBuilder Create(Action configure) => new HostBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes); #else public static IWebHostBuilder Create(Action configure) => new WebHostBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes); #endif public static void WithEnabledValidateScopes(ServiceProviderOptions options) => options.ValidateScopes = true; public static IHostBuilder CreateHost() => Host.CreateDefaultBuilder().UseDefaultServiceProvider(WithEnabledValidateScopes); public static IHostBuilder CreateHost(Action configure) => Host.CreateDefaultBuilder().UseDefaultServiceProvider(configure + WithEnabledValidateScopes); } ================================================ FILE: testing/TestWebBuilder.cs ================================================ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; namespace Ocelot.Testing; public static class TestWebBuilder { public static void WithEnabledValidateScopes(ServiceProviderOptions options) => options.ValidateScopes = true; public static WebApplicationBuilder CreateBuilder() { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes); return builder; } public static WebApplicationBuilder Create(Action configure) { var builder = WebApplication.CreateBuilder(); builder.WebHost.UseDefaultServiceProvider(configure + WithEnabledValidateScopes); return builder; } public static WebApplicationBuilder CreateBuilder(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes); return builder; } public static WebApplicationBuilder CreateSlimBuilder() { var builder = WebApplication.CreateSlimBuilder(); builder.WebHost.UseDefaultServiceProvider(WithEnabledValidateScopes); return builder; } } ================================================ FILE: testing/Unit.cs ================================================ using Ocelot.Infrastructure.Extensions; using System.Runtime.CompilerServices; namespace Ocelot.Testing; /// /// This is the base class for any unit testing classes. /// It is recommended to always inherit from it. /// public class Unit { protected readonly Guid _testId = Guid.NewGuid(); protected string TestID { get => _testId.ToString("N"); } protected string TestName([CallerMemberName] string? testName = null) => testName.IfEmpty(TestID); protected virtual bool IsCiCd() => IsRunningInGitHubActions(); protected static bool IsRunningInGitHubActions() => Environment.GetEnvironmentVariable("GITHUB_ACTIONS") == "true"; } ================================================ FILE: testing/Wait.cs ================================================ using System.Diagnostics; namespace Ocelot.Testing; public class Wait { private readonly int _milliSeconds; public static Wait For(int milliSeconds) => new(milliSeconds); private Wait() { } private Wait(int milliSeconds) { _milliSeconds = milliSeconds; } public bool Until(Func condition) { var watcher = Stopwatch.StartNew(); while (watcher.ElapsedMilliseconds < _milliSeconds) { if (condition.Invoke()) { watcher.Stop(); return true; } } watcher.Stop(); return false; } public async Task UntilAsync(Func> condition) { var watcher = Stopwatch.StartNew(); while (watcher.ElapsedMilliseconds < _milliSeconds) { if (await condition.Invoke()) { watcher.Stop(); return true; } } watcher.Stop(); return false; } }