Skip to content

Commit

Permalink
#1225 Update ServiceDiscovery documentation and samples to include Cu…
Browse files Browse the repository at this point in the history
…stom 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 <dotnet044@gmail.com>
  • Loading branch information
leonluc-dev and raman-m committed Sep 28, 2023
1 parent fa179bf commit 190b001
Show file tree
Hide file tree
Showing 27 changed files with 792 additions and 7 deletions.
19 changes: 18 additions & 1 deletion Ocelot.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
132 changes: 126 additions & 6 deletions docs/features/servicediscovery.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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 <https://github.com/ThreeMammals/Ocelot/issues/262>`_ . to add support for Netflix's Eureka service discovery provider. The main reason for this is it is a key part of `Steeltoe <https://steeltoe.io/>`_ which is something to do with `Pivotal <https://pivotal.io/platform>`_! 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.

Expand Down Expand Up @@ -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 <https://github.com/ThreeMammals/Ocelot/issues/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.

Expand Down Expand Up @@ -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<List<Service>> Get()
{
var services = new List<Service>();
//...
//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<ServiceDiscoveryFinderDelegate>((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<IServiceDiscoveryProviderFactory>();
s.AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();
// It will not be called, but it is necessary for internal validators, it is also a lifehack
s.AddSingleton<ServiceDiscoveryFinderDelegate>((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.
25 changes: 25 additions & 0 deletions samples/OcelotServiceDiscovery/.dockerignore
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Ocelot\Ocelot.csproj" />
</ItemGroup>

</Project>
63 changes: 63 additions & 0 deletions samples/OcelotServiceDiscovery/ApiGateway/Program.cs
Original file line number Diff line number Diff line change
@@ -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<ServiceDiscoveryFinderDelegate>((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<IServiceDiscoveryProviderFactory>();
s.AddSingleton<IServiceDiscoveryProviderFactory, MyServiceDiscoveryProviderFactory>();
// Will not be called, but it is required for internal validators, aka life hack
s.AddSingleton<ServiceDiscoveryFinderDelegate>((serviceProvider, config, downstreamRoute)
=> null);
}
s.AddOcelot();
})
.Configure(a =>
{
a.UseOcelot().Wait();
})
.Build();
}
Original file line number Diff line number Diff line change
@@ -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/"
}
}
}
Original file line number Diff line number Diff line change
@@ -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<List<Service>> Get()
{

// Returns a list of service(s) that match the downstream route passed to the provider
var services = new List<Service>();

// 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);
}
}

0 comments on commit 190b001

Please sign in to comment.