From 190b00176376671bf0c7b86cc40fe8aaea619745 Mon Sep 17 00:00:00 2001 From: leonluc-dev Date: Thu, 28 Sep 2023 18:14:43 +0200 Subject: [PATCH] #1225 Update ServiceDiscovery documentation and samples to include Custom Providers (#1656) * Update servicediscovery documentation to include custom provider * Update servicediscovery.rst Custom Providers paragraph * Update servicediscovery.rst Fix lower/upper case in paragraph name * Added custom service provider sample * Minor clarification to custom service discovery provider docs * Move usings to the top. Use file-scoped namespace declaration * Moved custom service discovery sample Added sample to Ocelot.sln * Added custom service provider sample * Move usings to the top. Use file-scoped namespace declaration * Moved custom service discovery sample Added sample to Ocelot.sln * Add 2 options/ways of solution development via ConfigureServices * Upgrade DownstreamService to ASP.NET 7 * Upgrade ApiGateway to ASP.NET 7 * Update README.md * Removed redundant spring section in config Move urls config from Program.cs to appsettings.json * Workaround for the Categories route * Rename controller: class name should be the same as file name * Upgrade to Web API app: multiple startup profiles, add Docker profile. Basic microservices template * Correct registration of IServiceDiscoveryProviderFactory interface * Update README.md: Fix upstream path because of case sensitivity * Update servicediscovery.rst: Update Custom Providers section. Add sample solution. * Update servicediscovery.rst: Update actual code from the sample * Remove obsolete code * CS8632 The annotation for nullable reference types should only be used in code within a '#nullable' annotations context * Revert to previous state * Revert back to the version from ThreeMammals:develop * Update servicediscovery.rst: English checking --------- Co-authored-by: raman-m --- Ocelot.sln | 19 ++- docs/features/servicediscovery.rst | 132 +++++++++++++++++- samples/OcelotServiceDiscovery/.dockerignore | 25 ++++ ...Samples.ServiceDiscovery.ApiGateway.csproj | 11 ++ .../ApiGateway/Program.cs | 63 +++++++++ .../ApiGateway/Properties/launchSettings.json | 28 ++++ .../MyServiceDiscoveryProvider.cs | 47 +++++++ .../MyServiceDiscoveryProviderFactory.cs | 31 ++++ .../ApiGateway/appsettings.json | 11 ++ .../ApiGateway/ocelot.json | 19 +++ .../DownstreamService/.dockerignore | 25 ++++ .../Controllers/CategoriesController.cs | 24 ++++ .../Controllers/HealthController.cs | 51 +++++++ .../Controllers/WeatherForecastController.cs | 32 +++++ .../DownstreamService/Dockerfile | 22 +++ .../DownstreamService/Models/HealthResult.cs | 6 + .../Models/MicroserviceResult.cs | 6 + .../DownstreamService/Models/ReadyResult.cs | 10 ++ .../Models/WeatherForecast.cs | 12 ++ ....ServiceDiscovery.DownstreamService.csproj | 18 +++ .../DownstreamService/Program.cs | 17 +++ .../Properties/launchSettings.json | 48 +++++++ .../DownstreamService/Startup.cs | 64 +++++++++ .../appsettings.Development.json | 8 ++ .../DownstreamService/appsettings.json | 9 ++ .../Ocelot.Samples.ServiceDiscovery.sln | 31 ++++ samples/OcelotServiceDiscovery/README.md | 30 ++++ 27 files changed, 792 insertions(+), 7 deletions(-) create mode 100644 samples/OcelotServiceDiscovery/.dockerignore create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/Program.cs create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/appsettings.json create mode 100644 samples/OcelotServiceDiscovery/ApiGateway/ocelot.json create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/.dockerignore create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Dockerfile create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Program.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/Startup.cs create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json create mode 100644 samples/OcelotServiceDiscovery/DownstreamService/appsettings.json create mode 100644 samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln create mode 100644 samples/OcelotServiceDiscovery/README.md diff --git a/Ocelot.sln b/Ocelot.sln index b59c188a2..8ed0e2067 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.0.32112.339 +VisualStudioVersion = 17.6.33723.286 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5CFB79B7-C9DC-45A4-9A75-625D92471702}" EndProject @@ -86,6 +86,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "open-tracing", "open-tracin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OcelotOpenTracing", "samples\OcelotOpenTracing\OcelotOpenTracing.csproj", "{C9427E78-4281-4F59-A66E-17C0B66550E5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "service-discovery", "service-discovery", "{25C30AAA-12DD-4BA5-A53F-9271E54EBAB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.ApiGateway", "samples\OcelotServiceDiscovery\ApiGateway\Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj", "{D37209EA-C13E-42AE-B851-A8604F1FCD0E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -188,6 +194,14 @@ Global {C9427E78-4281-4F59-A66E-17C0B66550E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9427E78-4281-4F59-A66E-17C0B66550E5}.Release|Any CPU.Build.0 = Release|Any CPU + {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D37209EA-C13E-42AE-B851-A8604F1FCD0E}.Release|Any CPU.Build.0 = Release|Any CPU + {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -224,6 +238,9 @@ Global {11C622AD-8C0A-4CF4-811B-3DBB76550797} = {5CFB79B7-C9DC-45A4-9A75-625D92471702} {731C6A8A-69ED-445C-A132-C638AA93F9C7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {C9427E78-4281-4F59-A66E-17C0B66550E5} = {731C6A8A-69ED-445C-A132-C638AA93F9C7} + {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} + {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} + {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/docs/features/servicediscovery.rst b/docs/features/servicediscovery.rst index 9b2b158bd..e32e7cd0e 100644 --- a/docs/features/servicediscovery.rst +++ b/docs/features/servicediscovery.rst @@ -7,11 +7,13 @@ Ocelot allows you to specify a service discovery provider and will use this to f GlobalConfiguration section which means the same service discovery provider will be used for all Routes you specify a ServiceName for at Route level. Consul -^^^^^^ +------ The first thing you need to do is install the NuGet package that provides Consul support in Ocelot. -``Install-Package Ocelot.Provider.Consul`` +.. code-block:: powershell + + Install-Package Ocelot.Provider.Consul Then add the following to your ConfigureServices method. @@ -92,7 +94,7 @@ Or } ACL Token ---------- +^^^^^^^^^ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token header. In order so this to work you must add the additional property below. @@ -108,13 +110,15 @@ If you are using ACL with Consul Ocelot supports adding the X-Consul-Token heade Ocelot will add this token to the Consul client that it uses to make requests and that is then used for every request. Eureka -^^^^^^ +------ This feature was requested as part of `Issue 262 `_ . to add support for Netflix's Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe `_ which is something to do with `Pivotal `_! Anyway enough of the background. The first thing you need to do is install the NuGet package that provides Eureka support in Ocelot. -``Install-Package Ocelot.Provider.Eureka`` +.. code-block:: powershell + + Install-Package Ocelot.Provider.Eureka Then add the following to your ConfigureServices method. @@ -150,7 +154,7 @@ Ocelot will now register all the necessary services when it starts up and if you Ocelot will use the scheme (http/https) set in Eureka if these values are not provided in ocelot.json Dynamic Routing -^^^^^^^^^^^^^^^ +--------------- This feature was requested in `issue 340 `_. The idea is to enable dynamic routing when using a service discovery provider (see that section of the docs for more info). In this mode Ocelot will use the first segment of the upstream path to lookup the downstream service with the service discovery provider. @@ -242,3 +246,119 @@ Ocelot also allows you to set DynamicRoutes which lets you set rate limiting rul This configuration means that if you have a request come into Ocelot on /product/* then dynamic routing will kick in and ocelot will use the rate limiting set against the product service in the DynamicRoutes section. Please take a look through all of the docs to understand these options. + +Custom Providers +---------------------------------- + +Ocelot also allows you to create your own ServiceDiscovery implementation. +This is done by implementing the ``IServiceDiscoveryProvider`` interface, as shown in the following example: + +.. code-block:: csharp + + public class MyServiceDiscoveryProvider : IServiceDiscoveryProvider + { + private readonly DownstreamRoute _downstreamRoute; + + public MyServiceDiscoveryProvider(DownstreamRoute downstreamRoute) + { + _downstreamRoute = downstreamRoute; + } + + public async Task> Get() + { + 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 application's **ConfigureServices** method, register a ``ServiceDiscoveryFinderDelegate`` to initialize and return the provider: + +.. code-block:: csharp + + ServiceDiscoveryFinderDelegate serviceDiscoveryFinder = (provider, config, route) => + { + return new MyServiceDiscoveryProvider(route); + }; + services.AddSingleton(serviceDiscoveryFinder); + services.AddOcelot(); + +Custom Provider Sample +^^^^^^^^^^^^^^^^^^^^^^ + +In order to introduce a basic template for a custom Service Discovery provider, we've prepared a good sample: + + | **Link**: `samples <../../samples>`_ / `OcelotServiceDiscovery <../../samples/OcelotServiceDiscovery>`_ + | **Solution**: `Ocelot.Samples.ServiceDiscovery.sln <../../samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln>`_ + +This solution contains the following projects: + +- `ApiGateway <#apigateway>`_ +- `DownstreamService <#downstreamservice>`_ + +This solution is ready for any deployment. All services are bound, meaning all ports and hosts are prepared for immediate use (running in Visual Studio). + +All instructions for running this solution are in `README.md <../../samples/OcelotServiceDiscovery/README.md>`_. + +DownstreamService +""""""""""""""""" + +This project provides a single downstream service that can be reused across `ApiGateway <#apigateway>`_ routes. +It has multiple **launchSettings.json** profiles for your favorite launch and hosting scenarios: Visual Studio running sessions, Kestrel console hosting, and Docker deployments. + +ApiGateway +"""""""""" + +This project includes a custom Service Discovery provider and it only has route(s) to `DownstreamService <#downstreamservice>`_ services in the **ocelot.json** file. +You can add more routes! + +The main source code for the custom provider is in the `ServiceDiscovery <../../samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery>`_ folder: +the ``MyServiceDiscoveryProvider`` and ``MyServiceDiscoveryProviderFactory`` classes. You are welcome to design and develop them! + +Additionally, the cornerstone of this custom provider is the ``ConfigureServices`` method, where you can choose design and implementation options: simple or more complex: + +.. code-block:: csharp + + builder.ConfigureServices(s => + { + // 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, which is ServiceDiscoveryProviderFactory + s.AddSingleton((serviceProvider, config, downstreamRoute) + => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute)); + } + else + { + // Design #2. Abstract from the default factory (ServiceDiscoveryProviderFactory) and from FinderDelegate, + // and create your own factory by implementing the IServiceDiscoveryProviderFactory interface. + s.RemoveAll(); + s.AddSingleton(); + + // It will not be called, but it is necessary for internal validators, it is also a lifehack + s.AddSingleton((serviceProvider, config, downstreamRoute) => null); + } + + s.AddOcelot(); + }); + +The easy way, lite design means that you only design the provider class, and specify ``ServiceDiscoveryFinderDelegate`` object for default ``ServiceDiscoveryProviderFactory`` in Ocelot core. + +A more complex design means that you design both provider and provider factory classes. +After this, you need to add the ``IServiceDiscoveryProviderFactory`` interface to the DI-container, removing the default registered ``ServiceDiscoveryProviderFactory`` class. +Note that in this case the Ocelot core will not use ``ServiceDiscoveryProviderFactory`` by default. +Additionally, you do not need to specify ``"Type": "MyServiceDiscoveryProvider"`` in the **ServiceDiscoveryProvider** properties of the **GlobalConfiguration** settings. +But you can leave this ``Type`` option for compatibility between both designs. diff --git a/samples/OcelotServiceDiscovery/.dockerignore b/samples/OcelotServiceDiscovery/.dockerignore new file mode 100644 index 000000000..e7b690f11 --- /dev/null +++ b/samples/OcelotServiceDiscovery/.dockerignore @@ -0,0 +1,25 @@ +**/.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 diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj b/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj new file mode 100644 index 000000000..e987ea143 --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/Ocelot.Samples.ServiceDiscovery.ApiGateway.csproj @@ -0,0 +1,11 @@ + + + + net7.0 + + + + + + + diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Program.cs b/samples/OcelotServiceDiscovery/ApiGateway/Program.cs new file mode 100644 index 000000000..1a0e37336 --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/Program.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Ocelot.DependencyInjection; +using Ocelot.Middleware; +using Ocelot.ServiceDiscovery; + +namespace Ocelot.Samples.ServiceDiscovery.ApiGateway; + +using ServiceDiscovery; + +public class Program +{ + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", true, true) + .AddJsonFile($"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + // Initialize from app configuration or hardcode/choose the best option. + bool easyWay = true; + + if (easyWay) + { + // Option #1. Define custom finder delegate to instantiate custom provider + // by default factory which is ServiceDiscoveryProviderFactory + s.AddSingleton((serviceProvider, config, downstreamRoute) + => new MyServiceDiscoveryProvider(serviceProvider, config, downstreamRoute)); + } + else + { + // Option #2. Abstract from default factory (ServiceDiscoveryProviderFactory) and from FinderDelegate, + // and build custom factory by implementation of the IServiceDiscoveryProviderFactory interface. + s.RemoveAll(); + s.AddSingleton(); + + // Will not be called, but it is required for internal validators, aka life hack + s.AddSingleton((serviceProvider, config, downstreamRoute) + => null); + } + + s.AddOcelot(); + }) + .Configure(a => + { + a.UseOcelot().Wait(); + }) + .Build(); +} diff --git a/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json b/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json new file mode 100644 index 000000000..4262362b0 --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54060/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ApiGateway": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "categories", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000/" + } + } +} diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs b/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs new file mode 100644 index 000000000..76e655bd1 --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProvider.cs @@ -0,0 +1,47 @@ +using Ocelot.Configuration; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +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> Get() + { + + // 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")) + { + //For this example we simply do a manual match to a single service + var service = new Service( + name: "downstream-service", + hostAndPort: new ServiceHostAndPort("localhost", 5001), + id: "downstream-service-1", + version: "1.0", + tags: new string[] { "downstream", "hardcoded" } + ); + + services.Add(service); + } + + return Task.FromResult(services); + } +} diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs b/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs new file mode 100644 index 000000000..1978ebd3d --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/ServiceDiscovery/MyServiceDiscoveryProviderFactory.cs @@ -0,0 +1,31 @@ +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery; +using Ocelot.ServiceDiscovery.Providers; +using System; + +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); + } +} diff --git a/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json b/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json new file mode 100644 index 000000000..9c1fbf602 --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "IncludeScopes": true, + "LogLevel": { + "Default": "Information", + "System": "Information", + "Microsoft": "Information" + } + }, + "Urls": "http://localhost:5000" +} diff --git a/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json b/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json new file mode 100644 index 000000000..7460dcc2a --- /dev/null +++ b/samples/OcelotServiceDiscovery/ApiGateway/ocelot.json @@ -0,0 +1,19 @@ +{ + "Routes": [ + { + "DownstreamPathTemplate": "/categories", + "DownstreamScheme": "http", + "UpstreamPathTemplate": "/categories", + "ServiceName": "downstream-service", + "UpstreamHttpMethod": [ "Get" ], + "FileCacheOptions": { "TtlSeconds": 15 } + } + ], + "GlobalConfiguration": { + "RequestIdKey": "OcRequestId", + "AdministrationPath": "/administration", + "ServiceDiscoveryProvider": { + "Type": "MyServiceDiscoveryProvider" + } + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore b/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore new file mode 100644 index 000000000..e7b690f11 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/.dockerignore @@ -0,0 +1,25 @@ +**/.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 diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs new file mode 100644 index 000000000..7cb6860f2 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/CategoriesController.cs @@ -0,0 +1,24 @@ +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; + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs new file mode 100644 index 000000000..3d351e461 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/HealthController.cs @@ -0,0 +1,51 @@ +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), + }; + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs new file mode 100644 index 000000000..2576dec59 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Controllers/WeatherForecastController.cs @@ -0,0 +1,32 @@ +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(); + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile b/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile new file mode 100644 index 000000000..b7535cfcd --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +WORKDIR /src +COPY ["Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "."] +RUN dotnet restore "./Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" +COPY . . +WORKDIR "/src/." +RUN dotnet build "Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Ocelot.Samples.ServiceDiscovery.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.ServiceDiscovery.DownstreamService.dll"] diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs b/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs new file mode 100644 index 000000000..3d017443a --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Models/HealthResult.cs @@ -0,0 +1,6 @@ +namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; + +public class HealthResult : MicroserviceResult +{ + public bool Healthy { get; set; } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs b/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs new file mode 100644 index 000000000..0da25223b --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Models/MicroserviceResult.cs @@ -0,0 +1,6 @@ +namespace Ocelot.Samples.ServiceDiscovery.DownstreamService.Models; + +public class MicroserviceResult +{ + public Uri Next { get; set; } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs b/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs new file mode 100644 index 000000000..040a3c2ad --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Models/ReadyResult.cs @@ -0,0 +1,10 @@ +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; } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs b/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs new file mode 100644 index 000000000..06622a9ec --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Models/WeatherForecast.cs @@ -0,0 +1,12 @@ +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; } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj b/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj new file mode 100644 index 000000000..b7c731bea --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + disable + enable + Linux + . + d5492aa8-b50c-41ae-a044-9954846db9ac + + + + + + + + + diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Program.cs b/samples/OcelotServiceDiscovery/DownstreamService/Program.cs new file mode 100644 index 000000000..2cfe402d1 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Program.cs @@ -0,0 +1,17 @@ +global using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore; + +[assembly: ApiController] + +namespace Ocelot.Samples.ServiceDiscovery.DownstreamService; + +public class Program +{ + public static void Main(string[] args) + { + WebHost.CreateDefaultBuilder(args) + .UseStartup() + .Build() + .Run(); + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json b/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json new file mode 100644 index 000000000..a6ccfad22 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Properties/launchSettings.json @@ -0,0 +1,48 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5001" + }, + "https": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7001;http://localhost:5001" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "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:65100", + "sslPort": 44373 + } + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs b/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs new file mode 100644 index 000000000..e605c7ba3 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/Startup.cs @@ -0,0 +1,64 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Ocelot.Samples.ServiceDiscovery.DownstreamService; + +public class Startup +{ + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + .AddEndpointsApiExplorer() + .AddSwaggerGen() + + .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; + }); + + AddApplicationServices(services); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHttpsRedirection(); + } + + app.UseRouting(); + app.UseEndpoints(configure => + { + configure.MapControllers(); + }); + } + + private static void AddApplicationServices(IServiceCollection services) + { + services.AddHttpClient(); // to keep performance of HTTP Client high + //services.AddSingleton + //services.AddScoped + //services.AddTransient + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json b/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json new file mode 100644 index 000000000..b0bacf428 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json b/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json new file mode 100644 index 000000000..223027717 --- /dev/null +++ b/samples/OcelotServiceDiscovery/DownstreamService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln b/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln new file mode 100644 index 000000000..299c18b54 --- /dev/null +++ b/samples/OcelotServiceDiscovery/Ocelot.Samples.ServiceDiscovery.sln @@ -0,0 +1,31 @@ + +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 diff --git a/samples/OcelotServiceDiscovery/README.md b/samples/OcelotServiceDiscovery/README.md new file mode 100644 index 000000000..40fd42a8d --- /dev/null +++ b/samples/OcelotServiceDiscovery/README.md @@ -0,0 +1,30 @@ +# 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" ] +} +```