Discovering Services
Depending on which Discovery service technology (e.g. Eureka or Consul) you are using the behavior of the client differs.
With Eureka, once the app starts, the client begins to operate in the background, both registering and renewing service registrations and also periodically fetching the service registry from the server.
With Consul, once the app starts, the client registers any services if required and if configured starts a health thread to keep updating the health of the service registration. No service registrations are fetched by the Consul client until you ask to lookup a service. At that point a request is made of the Consul server. As a result, you will probably want to use the Steeltoe caching load balancer with the Consul service discovery.
DiscoveryHttpClientHandler
A simple way to use the registry to lookup services is to use the Steeltoe DiscoveryHttpClientHandler
with HttpClient
.
This FortuneService
class retrieves fortunes from the Fortune microservice, which is registered under a name of fortuneService
:
using Steeltoe.Discovery.Client;
...
public class FortuneService : IFortuneService
{
DiscoveryHttpClientHandler _handler;
private const string RANDOM_FORTUNE_URL = "https://fortuneService/api/fortunes/random";
public FortuneService(IDiscoveryClient client)
{
_handler = new DiscoveryHttpClientHandler(client);
}
public async Task<string> RandomFortuneAsync()
{
var client = GetClient();
return await client.GetStringAsync(RANDOM_FORTUNE_URL);
}
private HttpClient GetClient()
{
// WARNING: do NOT create a new HttpClient for every request in your code
// -- you may experience socket exhaustion if you do!
var client = new HttpClient(_handler, false);
return client;
}
}
First, notice that the FortuneService
constructor takes an IDiscoveryClient
as a parameter. This is Steeltoe's interface for finding services in the service registry. The IDiscoveryClient
implementation is registered with the service container for use in any controller, view, or service during initialization. The constructor code for this class uses the client to create an instance of Steeltoe's DiscoveryHttpClientHandler
.
Next, notice that when the RandomFortuneAsync()
method is called, the HttpClient
is created with the Steeltoe handler. The handler's role is to intercept any requests made with the HttpClient
and to evaluate the URL to see if the host portion of the URL can be resolved from the service registry. In this example, the fortuneService
name should be resolved into an actual host:port
before letting the request continue.
If the name cannot be resolved, the handler ignores the request URL and lets the request continue unchanged.
NOTE:
DiscoveryHttpClientHandler
performs random load balancing by default. That is, if there are multiple instances registered under a particular service name, the handler randomly selects one of those instances each time the handler is invoked.
Using HttpClientFactory
In addition to the DiscoveryHttpClientHandler
mentioned above, you also have the option to use the new HttpClientFactory together with the Steeltoe provided DiscoveryHttpMessageHandler
for service lookup.
DiscoveryHttpMessageHandler
is a DelegatingHandler
that can be used, much like the DiscoveryHttpClientHandler
, to intercept requests and to evaluate the URL to see if the host portion of the URL can be resolved from the current service registry. The handler will do this for any HttpClient
created by the factory.
After initializing the discovery client, you can easily configure HttpClient
:
public class Startup
{
...
public void ConfigureServices(IServiceCollection services)
{
...
// Add Steeltoe handler to container (this line can be omitted when using Steeltoe versions >= 2.2.0)
services.AddTransient<DiscoveryHttpMessageHandler>();
// Configure HttpClient
services.AddHttpClient("fortunes", c =>
{
c.BaseAddress = new Uri("https://fortuneService/api/fortunes/");
})
.AddHttpMessageHandler<DiscoveryHttpMessageHandler>()
.AddTypedClient<IFortuneService, FortuneService>();
...
}
...
}
The updated version of FortuneService
is a bit simpler:
public class FortuneService : IFortuneService
{
private const string RANDOM_FORTUNE_URL = "https://fortuneService/api/fortunes/random";
private HttpClient _client;
public FortuneService(HttpClient client)
{
_client = client;
}
public async Task<string> RandomFortuneAsync()
{
return await _client.GetStringAsync(RANDOM_FORTUNE_URL);
}
}
Check out the Microsoft documentation on HttpClientFactory to see all the various ways you can make use of message handlers.
NOTE:
DiscoveryHttpMessageHandler
has an optionalILoadBalancer
parameter. If noILoadBalancer
is provided via dependency injection, aRandomLoadBalancer
is used. To change this behavior, add anILoadBalancer
to the DI container or use a load-balancer first configuration as described within section 1.4 on this page.
Using IDiscoveryClient
In the event the handler options don't serve your needs, you can always make lookup requests directly on the IDiscoveryClient
interface.
These methods available on an IDiscoveryClient
provide access to services and service instances available in the registry:
/// <summary>
/// Gets all known service Ids
/// </summary>
IList<string> Services { get; }
/// <summary>
/// Get all ServiceInstances associated with a particular serviceId
/// </summary>
/// <param name="serviceId">the serviceId to lookup</param>
/// <returns>List of service instances</returns>
IList<IServiceInstance> GetInstances(string serviceId);