-
Notifications
You must be signed in to change notification settings - Fork 10k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
API Proposal: Microsoft.Extensions.ServiceDiscovery #53715
Comments
cc @halter73 |
API Review Notes:
|
Here is an updated proposal with a reduced surface area, and reflecting the discussion from the preview meeting. The only thing which is not here is Microsoft.Extensions.ServiceDiscovery.Abstractions.dll namespace Microsoft.Extensions.ServiceDiscovery {
public interface IHostNameFeature {
string HostName { get; }
}
public interface IServiceEndPointBuilder {
IList<ServiceEndPoint> EndPoints { get; }
IFeatureCollection Features { get; }
void AddChangeToken(IChangeToken changeToken);
}
public interface IServiceEndPointProvider : IAsyncDisposable {
ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken);
}
public interface IServiceEndPointProviderFactory {
bool TryCreateProvider(ServiceEndPointQuery query, out IServiceEndPointProvider? resolver);
}
public abstract class ServiceEndPoint {
protected ServiceEndPoint();
public abstract EndPoint EndPoint { get; }
public abstract IFeatureCollection Features { get; }
public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null);
public virtual string GetEndPointString();
}
public sealed class ServiceEndPointQuery {
public string? EndPointName { get; }
public IReadOnlyList<string> IncludeSchemes { get; }
public string OriginalString { get; }
public string ServiceName { get; }
public override string? ToString();
public static bool TryParse(string input, out ServiceEndPointQuery? query);
}
public sealed class ServiceEndPointSource {
public ServiceEndPointSource(List<ServiceEndPoint>? endpoints, IChangeToken changeToken, IFeatureCollection features);
public IChangeToken ChangeToken { get; }
public IReadOnlyList<ServiceEndPoint> EndPoints { get; }
public IFeatureCollection Features { get; }
public override string ToString();
}
} Microsoft.Extensions.ServiceDiscovery.dll namespace Microsoft.Extensions.DependencyInjection {
public static class ServiceDiscoveryHttpClientBuilderExtensions {
public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder);
}
public static class ServiceDiscoveryServiceCollectionExtensions {
public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services);
public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action<ConfigurationServiceEndPointResolverOptions>? configureOptions);
public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services);
public static IServiceCollection AddServiceDiscovery(this IServiceCollection services);
public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action<ServiceDiscoveryOptions>? configureOptions);
public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services);
public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action<ServiceDiscoveryOptions>? configureOptions);
}
}
namespace Microsoft.Extensions.ServiceDiscovery {
public sealed class ConfigurationServiceEndPointResolverOptions {
public ConfigurationServiceEndPointResolverOptions();
public Func<ServiceEndPoint, bool> ApplyHostNameMetadata { get; set; }
public string SectionName { get; set; }
}
public sealed class ServiceDiscoveryOptions {
public static readonly string[] AllowAllSchemes;
public ServiceDiscoveryOptions();
public string[] AllowedSchemes { get; set; }
public TimeSpan RefreshPeriod { get; set; }
}
public sealed class ServiceEndPointResolver : IAsyncDisposable {
public ValueTask DisposeAsync();
public ValueTask<ServiceEndPointSource> GetEndPointsAsync(string serviceName, CancellationToken cancellationToken);
}
}
namespace Microsoft.Extensions.ServiceDiscovery.Http {
public interface IServiceDiscoveryHttpMessageHandlerFactory {
HttpMessageHandler CreateHandler(HttpMessageHandler handler);
}
} |
API Review Notes:
API Approved! namespace Microsoft.Extensions.ServiceDiscovery {
public interface IHostNameFeature {
string HostName { get; }
}
public interface IServiceEndpointBuilder {
IList<ServiceEndpoint> Endpoints { get; }
IFeatureCollection Features { get; }
void AddChangeToken(IChangeToken changeToken);
}
public interface IServiceEndpointProvider : IAsyncDisposable {
ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken);
}
public interface IServiceEndpointProviderFactory {
bool TryCreateProvider(ServiceEndpointQuery query, out IServiceEndpointProvider? resolver);
}
public abstract class ServiceEndpoint {
protected ServiceEndpoint();
public abstract EndPoint EndPoint { get; }
public abstract IFeatureCollection Features { get; }
public static ServiceEndpoint Create(Endpoint endpoint, IFeatureCollection? features = null);
public virtual string GetEndpointString();
}
public sealed class ServiceEndpointQuery {
public string? EndpointName { get; }
public IReadOnlyList<string> IncludedSchemes { get; }
public string ServiceName { get; }
public override string? ToString();
public static bool TryParse(string input, out ServiceEndpointQuery? query);
}
public sealed class ServiceEndpointSource {
public ServiceEndpointSource(List<ServiceEndpoint>? endpoints, IChangeToken changeToken, IFeatureCollection features);
public IChangeToken ChangeToken { get; }
public IReadOnlyList<ServiceEndpoint> Endpoints { get; }
public IFeatureCollection Features { get; }
public override string ToString();
}
}
namespace Microsoft.Extensions.DependencyInjection {
public static class ServiceDiscoveryHttpClientBuilderExtensions {
public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder);
}
public static class ServiceDiscoveryServiceCollectionExtensions {
public static IServiceCollection AddConfigurationServiceEndpointResolver(this IServiceCollection services);
public static IServiceCollection AddConfigurationServiceEndpointResolver(this IServiceCollection services, Action<ConfigurationServiceEndpointResolverOptions> configureOptions);
public static IServiceCollection AddPassThroughServiceEndpointResolver(this IServiceCollection services);
public static IServiceCollection AddServiceDiscovery(this IServiceCollection services);
public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action<ServiceDiscoveryOptions> configureOptions);
public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services);
public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action<ServiceDiscoveryOptions> configureOptions);
}
}
namespace Microsoft.Extensions.ServiceDiscovery {
public sealed class ConfigurationServiceEndpointResolverOptions {
public ConfigurationServiceEndpointResolverOptions();
public Func<ServiceEndpoint, bool> ShouldApplyHostNameMetadata { get; set; }
public string SectionName { get; set; }
}
public sealed class ServiceDiscoveryOptions {
public ServiceDiscoveryOptions();
public bool AllowAllSchemes { get; set; }
public IList<string> AllowedSchemes { get; set; }
public TimeSpan RefreshPeriod { get; set; }
}
public sealed class ServiceEndpointResolver : IAsyncDisposable {
public ValueTask DisposeAsync();
public ValueTask<ServiceEndpointSource> GetEndpointsAsync(string serviceName, CancellationToken cancellationToken);
}
}
namespace Microsoft.Extensions.ServiceDiscovery.Http {
public interface IServiceDiscoveryHttpMessageHandlerFactory {
HttpMessageHandler CreateHandler(HttpMessageHandler handler);
}
} |
Addendum, we removed |
Hi @ReubenBond var endPointResolverFactory = serviceProvider.GetRequiredService<ServiceEndPointResolverFactory>();
var endpoints = await endPointResolverFactory
.CreateResolver(serviceName)
.GetEndPointsAsync();
return endpoints.Select(endpoint => endpoint.GetEndPointString()).FirstOrDefault(); |
@gdantuono the equivalent now is this: var resolver = services.GetRequiredService<ServiceEndpointResolver>();
var endpoints = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false);
return endpoints.Endpoints?[0].ToString(); |
@ReubenBond thanks you, very much. This code also handles no results: var source = await endPointResolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false);
return (source.EndPoints?.Count > 0) ? source.EndPoints[0].ToString() : null; |
Background and Motivation
We added Service Discovery APIs for Aspire to improve the dev inner loop experience for multi-service apps. The core idea behind these APIs is to give developers a way to identify services using logical names (eg,
"https://catalog"
) in both development and production environments, with the Service Discovery library performing the work of translating those logical names into something which a network connection can be established with. The core duties of the Service Discovery APIs are:The API should be extensible enough to support the most common service discovery mechanisms:
Service
resources can be used to expose the addresses and ports of services via DNS SRV records. This allows for multiple, named ports on service. For example, this allows a developer can query for "the dashboard port of the catalog service".catalog
, the corresponding ACA app namedcatalog
will be resolved).The API also aims to support:
HttpClient
instances which have been configured with Service Discovery.IDestinationResolver
API.The primary touch points for developers are:
HttpClient
.High level design
The library is designed around a provider model. Integration with
HttpClient
is implemented using a middleware,ResolvingHttpClientMiddleware
. Here's the approximate flow for resolving a, reading depth-first from top to bottom, then left to right.Key design notes
System.Net.EndPoint
. It is presented to consumers via a new type,ServiceEndPoint
which contains two properties: theEndPoint
and anIFeatureCollection
property which can be used to attach metadata and functionality to exposed endpoints.IHostNameFeature
to the endpoint.HttpClient
via theHttpRequestMessage.Headers.Host
property.IServiceEndPointProvider
which resolved the endpoint to the endpoint for diagnosticsIEndPointHealthFeature
- not implemented.IEndPointLoadFeature
(eg, for Power-of-two-choices load balancing byIServiceEndPointSelector
) - not implemented, no resolvers currently expose load metrics.ServiceEndPointCollection
which implementsIReadOnlyList<ServiceEndPoint>
and adds:IChangeToken
property, which is used to signal when a collection should be refreshed. Providers invalidate the change token in several ways:IConfigurationSection
'sIChangeToken
is invalidatedIFeatureCollection
property to add per-collection features.string
)Proposed API
The API consists of 4 libraries, structured as follows:
The DNS & YARP libraries reference the core Service Discovery library for access to a hosting configuration method,
AddServiceDiscoveryCore()
, and in the case of YARP, forServiceEndPointResolver
, which is the central point for resolving endpoints from libraries.APIs in Microsoft.Extensions.ServiceDiscovery.Abstractions.dll
Microsoft.Extensions.ServiceDiscovery.Abstractions
APIs in Microsoft.Extensions.ServiceDiscovery.dll
Microsoft.Extensions.DependencyInjection
The rationale for putting these extension methods in this namespace is that this is where the
IHttpClientBuilder.AddStandardResilienceHandler()
extension lives. In Aspire apps, at least, these two often appear together in the ServiceDefaults project. Example:Microsoft.Extensions.Hosting
Microsoft.Extensions.ServiceDiscovery
Microsoft.Extensions.ServiceDiscovery.Abstractions
Microsoft.Extensions.ServiceDiscovery.Http
Usage Examples
Using Service Discovery with HttpClient
The primary scenario for service discovery is for
HttpClient
, whereA common approach is to add Service Discovery to the default
HttpClient
factory as follows. This example comes from the Aspire ServiceDefaults project which we generate on behalf of users:Once service discovery is configured on the application host, individual typed
HttpClient
s are configured by setting theBaseAddress
to include the logical service name. In the following example, there are two logical service names:The corresponding configuration sections in JSON might like this:
Named endpoints
Similarly, named endpoints can be used to specify a particular endpoint exposed by service, for example:
In configuration, each named endpoint has its own section and the configuration path takes the format:
$"services:{serviceName}:{endpointName}"
.To specify which endpoint should be resolved during consumption, the endpoint name is encoded into the input string, following the format:
$"_{endpointName}.{serviceName}"
.Scheme selection
During development time, it is currently much easier for developers to use plain-text HTTP. In production, however, this is not acceptable, and HTTPS is used instead. Therefore, we ideally want a way to allow developers to switch between HTTP at development time and HTTPS in production. The mechanism chosen for this is to allow specifying both schemes in the input string passed to service discovery, separated by a
+
symbol. For example,"https+http://catalogapi"
specifies that either HTTPS or HTTP can be used, and it is up to the Service Discovery system to select an appropriate scheme. Service Discovery evaluates these schemes in order and selects the first scheme which has corresponding configuration. For pass-through and DNS providers, the first scheme is always used.To limit the schemes permissible at runtime, an option is provided:
By default, all schemes are allowed, so the schemes specified in the input to service discovery will be used. These can be limited by specifying a set of allowed schemes, eg
["https"]
to allow only HTTPS.Ad-hoc service resolution
Service endpoints can also be resolved outside of
HttpClient
's integration. The most straight-forward approach to this is to useServiceEndPointResolver.GetEndPointsAsync(string serviceName, CancellationToken cancellationToken)
like so:Alternative Designs
Config-only: Scrap most of the library and only support host name replacement via
IConfiguration
. One benefit to that approach is that we could adopt a simpler, synchronous API. For the two main cases which we target with Aspire, that would be sufficient: we already useIConfiguration
in the inner-loop. When deployed to Azure Container Apps, we use the pass-thru provider since ACA has its own service discovery via a service mesh.Downsides to this approach include that it is not flexible or extensible outside of the
IConfiguration
extensibility model. Eg, it could not be extended to support purpose-built service discovery mechanisms like Consul and DNS or anything which requires I/O. It also would not support client-side load balancing.Risks & deficiencies
IFeatureCollection
allows for extensibility if we ever wanted to support this (and had a means to).IEndPointHealthFeature
and integrating it withIServiceEndPointSelector
.HttpClient
request. Eg, we cannot currently change the scheme from "http://" to "https://" using service discovery. We could offer this functionality using a feature on the returnedServiceEndPoint
or by returning aUriEndPoint
, which we do not currently have.IOptions<T>
or other DI services, in constructors, etc.Designer notes:
IEndPointLoadFeature.CurrentLoad
to afloat
(fromdouble
), since it has plenty of precision for the purposes of load balancing.The text was updated successfully, but these errors were encountered: