diff --git a/playground/AWS/AWS.ServiceDefaults/Extensions.cs b/playground/AWS/AWS.ServiceDefaults/Extensions.cs
index 65949135f6..675692cbc8 100644
--- a/playground/AWS/AWS.ServiceDefaults/Extensions.cs
+++ b/playground/AWS/AWS.ServiceDefaults/Extensions.cs
@@ -28,7 +28,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/playground/Playground.ServiceDefaults/Extensions.cs b/playground/Playground.ServiceDefaults/Extensions.cs
index 0a17073ea3..8a6a27b060 100644
--- a/playground/Playground.ServiceDefaults/Extensions.cs
+++ b/playground/Playground.ServiceDefaults/Extensions.cs
@@ -28,7 +28,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/playground/TestShop/ServiceDefaults/Extensions.cs b/playground/TestShop/ServiceDefaults/Extensions.cs
index 806cdc988a..755a9f7367 100644
--- a/playground/TestShop/ServiceDefaults/Extensions.cs
+++ b/playground/TestShop/ServiceDefaults/Extensions.cs
@@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/playground/orleans/OrleansServiceDefaults/Extensions.cs b/playground/orleans/OrleansServiceDefaults/Extensions.cs
index 7b654807f2..77a97dabc3 100644
--- a/playground/orleans/OrleansServiceDefaults/Extensions.cs
+++ b/playground/orleans/OrleansServiceDefaults/Extensions.cs
@@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/playground/seq/Seq.ServiceDefaults/Extensions.cs b/playground/seq/Seq.ServiceDefaults/Extensions.cs
index 0706f133be..42ff5356ef 100644
--- a/playground/seq/Seq.ServiceDefaults/Extensions.cs
+++ b/playground/seq/Seq.ServiceDefaults/Extensions.cs
@@ -29,7 +29,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs
deleted file mode 100644
index 63dc3e11a3..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Represents a feature that reports the health of an endpoint, for use in triggering internal cache refresh and for use in load balancing.
-///
-public interface IEndPointHealthFeature
-{
- ///
- /// Reports health of the endpoint, for use in triggering internal cache refresh and for use in load balancing. Can be a no-op.
- ///
- /// The response time of the endpoint.
- /// An optional exception that occurred while checking the endpoint's health.
- void ReportHealth(TimeSpan responseTime, Exception? exception);
-}
-
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs
deleted file mode 100644
index 2610f13594..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Represents a feature that provides information about the current load of an endpoint.
-///
-public interface IEndPointLoadFeature
-{
- ///
- /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc).
- ///
- public double CurrentLoad { get; }
-}
-
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs
similarity index 70%
rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs
rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs
index fff3c3fa3f..c748947237 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs
@@ -1,7 +1,7 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery;
///
/// Exposes the host name of the end point.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs
new file mode 100644
index 0000000000..468adea1c0
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.Extensions.ServiceDiscovery;
+
+///
+/// Builder to create a instances.
+///
+public interface IServiceEndPointBuilder
+{
+ ///
+ /// Gets the endpoints.
+ ///
+ IList EndPoints { get; }
+
+ ///
+ /// Gets the feature collection.
+ ///
+ IFeatureCollection Features { get; }
+
+ ///
+ /// Adds a change token to the resulting .
+ ///
+ /// The change token.
+ void AddChangeToken(IChangeToken changeToken);
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs
index 3b369a9785..950823257a 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs
@@ -1,7 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery;
///
/// Provides details about a service's endpoints.
@@ -14,5 +14,5 @@ public interface IServiceEndPointProvider : IAsyncDisposable
/// The endpoint collection, which resolved endpoints will be added to.
/// The token to monitor for cancellation requests.
/// The resolution status.
- ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken);
+ ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken);
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs
similarity index 59%
rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs
rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs
index 51343a5369..4b1876f808 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs
@@ -3,18 +3,18 @@
using System.Diagnostics.CodeAnalysis;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery;
///
/// Creates instances.
///
-public interface IServiceEndPointResolverProvider
+public interface IServiceEndPointProviderFactory
{
///
- /// Tries to create an instance for the specified .
+ /// Tries to create an instance for the specified .
///
- /// The service to create the resolver for.
+ /// The service to create the resolver for.
/// The resolver.
/// if the resolver was created, otherwise.
- bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver);
+ bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver);
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs
deleted file mode 100644
index 27f4ec4324..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Functionality for creating instances.
-///
-public interface IServiceEndPointSelectorProvider
-{
- ///
- /// Creates an instance.
- ///
- /// A new instance.
- IServiceEndPointSelector CreateSelector();
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs
index b73635ecd5..7d135dfe97 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs
@@ -4,21 +4,11 @@
using System.Net;
using Microsoft.AspNetCore.Http.Features;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.Internal;
-internal sealed class ServiceEndPointImpl : ServiceEndPoint
+internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint
{
- private readonly IFeatureCollection _features;
- private readonly EndPoint _endPoint;
-
- public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null)
- {
- _endPoint = endPoint;
- _features = features ?? new FeatureCollection();
- }
-
- public override EndPoint EndPoint => _endPoint;
- public override IFeatureCollection Features => _features;
-
+ public override EndPoint EndPoint { get; } = endPoint;
+ public override IFeatureCollection Features { get; } = features ?? new FeatureCollection();
public override string? ToString() => GetEndPointString();
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs
deleted file mode 100644
index 04eec95dc6..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Represents the status of an endpoint resolution operation.
-///
-public readonly struct ResolutionStatus(ResolutionStatusCode statusCode, Exception? exception, string message) : IEquatable
-{
- ///
- /// Indicates that resolution was not performed.
- ///
- public static readonly ResolutionStatus None = new(ResolutionStatusCode.None, exception: null, message: "");
-
- ///
- /// Indicates that resolution is ongoing and has not yet completed.
- ///
- public static readonly ResolutionStatus Pending = new(ResolutionStatusCode.Pending, exception: null, message: "Pending");
-
- ///
- /// Indicates that resolution has completed successfully.
- ///
- public static readonly ResolutionStatus Success = new(ResolutionStatusCode.Success, exception: null, message: "Success");
-
- ///
- /// Indicates that resolution was cancelled.
- ///
- public static readonly ResolutionStatus Cancelled = new(ResolutionStatusCode.Cancelled, exception: null, message: "Cancelled");
-
- ///
- /// Indicates that resolution did not find a result for the service.
- ///
- public static ResolutionStatus CreateNotFound(string message) => new(ResolutionStatusCode.NotFound, exception: null, message: message);
-
- ///
- /// Creates a status with a equal to with the provided exception.
- ///
- /// The resolution exception.
- /// A new instance.
- public static ResolutionStatus FromException(Exception exception)
- {
- ArgumentNullException.ThrowIfNull(exception);
- return new ResolutionStatus(ResolutionStatusCode.Error, exception, exception.Message);
- }
-
- ///
- /// Creates a status with a equal to with the provided exception.
- ///
- /// The resolution exception, if there was one.
- /// A new instance.
- public static ResolutionStatus FromPending(Exception? exception = null)
- {
- ArgumentNullException.ThrowIfNull(exception);
- return new ResolutionStatus(ResolutionStatusCode.Pending, exception, exception.Message);
- }
-
- ///
- /// Gets the resolution status code.
- ///
- public ResolutionStatusCode StatusCode { get; } = statusCode;
-
- ///
- /// Gets the resolution exception.
- ///
-
- public Exception? Exception { get; } = exception;
-
- ///
- /// Gets the resolution status message.
- ///
- public string Message { get; } = message;
-
- ///
- /// Compares the provided operands, returning if they are equal and if they are not equal.
- ///
- public static bool operator ==(ResolutionStatus left, ResolutionStatus right) => left.Equals(right);
-
- ///
- /// Compares the provided operands, returning if they are not equal and if they are equal.
- ///
- public static bool operator !=(ResolutionStatus left, ResolutionStatus right) => !(left == right);
-
- ///
- public override bool Equals(object? obj) => obj is ResolutionStatus status && Equals(status);
-
- ///
- public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode &&
- EqualityComparer.Default.Equals(Exception, other.Exception) &&
- Message == other.Message;
-
- ///
- public override int GetHashCode() => HashCode.Combine(StatusCode, Exception, Message);
-
- ///
- public override string ToString() => Exception switch
- {
- not null => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}, {nameof(Exception)}: {Exception}]",
- _ => $"[{nameof(StatusCode)}: {StatusCode}, {nameof(Message)}: {Message}]"
- };
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs
deleted file mode 100644
index 7157eac758..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Status codes for .
-///
-public enum ResolutionStatusCode
-{
- ///
- /// Resolution has not been performed.
- ///
- None = 0,
-
- ///
- /// Resolution is pending completion.
- ///
- Pending = 1,
-
- ///
- /// Resolution did not find any end points for the specified service.
- ///
- NotFound = 2,
-
- ///
- /// Resolution was successful.
- ///
- Success = 3,
-
- ///
- /// Resolution was canceled.
- ///
- Cancelled = 4,
-
- ///
- /// Resolution failed.
- ///
- Error = 5,
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs
index 9dc4675dad..a3cde62ce0 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs
@@ -4,8 +4,9 @@
using System.Diagnostics;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery;
///
/// Represents an endpoint for a service.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs
deleted file mode 100644
index 94f274a38e..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.AspNetCore.Http.Features;
-using Microsoft.Extensions.Primitives;
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// A mutable collection of service endpoints.
-///
-public class ServiceEndPointCollectionSource(string serviceName, IFeatureCollection features)
-{
- private readonly List _endPoints = new();
- private readonly List _changeTokens = new();
-
- ///
- /// Gets the service name.
- ///
- public string ServiceName { get; } = serviceName;
-
- ///
- /// Adds a change token.
- ///
- /// The change token.
- public void AddChangeToken(IChangeToken changeToken)
- {
- _changeTokens.Add(changeToken);
- }
-
- ///
- /// Gets the composite change token.
- ///
- /// The composite change token.
- public IChangeToken GetChangeToken() => new CompositeChangeToken(_changeTokens);
-
- ///
- /// Gets the feature collection.
- ///
- public IFeatureCollection Features { get; } = features;
-
- ///
- /// Gets the endpoints.
- ///
- public IList EndPoints => _endPoints;
-
- ///
- /// Creates a from the provided instance.
- ///
- /// The source collection.
- /// The service endpoint collection.
- public static ServiceEndPointCollection CreateServiceEndPointCollection(ServiceEndPointCollectionSource source)
- {
- return new ServiceEndPointCollection(source.ServiceName, source._endPoints, source.GetChangeToken(), source.Features);
- }
-}
-
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs
new file mode 100644
index 0000000000..99c92cce27
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.ServiceDiscovery;
+
+///
+/// Describes a query for endpoints of a service.
+///
+public sealed class ServiceEndPointQuery
+{
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The string which the query was constructed from.
+ /// The ordered list of included URI schemes.
+ /// The service name.
+ /// The optional endpoint name.
+ private ServiceEndPointQuery(string originalString, string[] includedSchemes, string serviceName, string? endPointName)
+ {
+ OriginalString = originalString;
+ IncludeSchemes = includedSchemes;
+ ServiceName = serviceName;
+ EndPointName = endPointName;
+ }
+
+ ///
+ /// Tries to parse the provided input as a service endpoint query.
+ ///
+ /// The value to parse.
+ /// The resulting query.
+ /// if the value was successfully parsed; otherwise .
+ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPointQuery? query)
+ {
+ bool hasScheme;
+ if (!input.Contains("://", StringComparison.InvariantCulture)
+ && Uri.TryCreate($"fakescheme://{input}", default, out var uri))
+ {
+ hasScheme = false;
+ }
+ else if (Uri.TryCreate(input, default, out uri))
+ {
+ hasScheme = true;
+ }
+ else
+ {
+ query = null;
+ return false;
+ }
+
+ var uriHost = uri.Host;
+ var segmentSeparatorIndex = uriHost.IndexOf('.');
+ string host;
+ string? endPointName = null;
+ if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.')
+ {
+ endPointName = uriHost[1..segmentSeparatorIndex];
+
+ // Skip the endpoint name, including its prefix ('_') and suffix ('.').
+ host = uriHost[(segmentSeparatorIndex + 1)..];
+ }
+ else
+ {
+ host = uriHost;
+ }
+
+ // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port".
+ var schemes = hasScheme ? uri.Scheme.Split('+') : [];
+ query = new(input, schemes, host, endPointName);
+ return true;
+ }
+
+ ///
+ /// Gets the string which the query was constructed from.
+ ///
+ public string OriginalString { get; }
+
+ ///
+ /// Gets the ordered list of included URI schemes.
+ ///
+ public IReadOnlyList IncludeSchemes { get; }
+
+ ///
+ /// Gets the endpoint name, or if no endpoint name is specified.
+ ///
+ public string? EndPointName { get; }
+
+ ///
+ /// Gets the service name.
+ ///
+ public string ServiceName { get; }
+
+ ///
+ public override string? ToString() => EndPointName is not null ? $"Service: {ServiceName}, Endpoint: {EndPointName}, Schemes: {string.Join(", ", IncludeSchemes)}" : $"Service: {ServiceName}, Schemes: {string.Join(", ", IncludeSchemes)}";
+}
+
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs
deleted file mode 100644
index 9179ed2f11..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Diagnostics.CodeAnalysis;
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Represents the result of service endpoint resolution.
-///
-/// The endpoint collection.
-/// The status.
-public sealed class ServiceEndPointResolverResult(ServiceEndPointCollection? endPoints, ResolutionStatus status)
-{
- ///
- /// Gets the status.
- ///
- public ResolutionStatus Status { get; } = status;
-
- ///
- /// Gets a value indicating whether resolution completed successfully.
- ///
- [MemberNotNullWhen(true, nameof(EndPoints))]
- public bool ResolvedSuccessfully => Status.StatusCode is ResolutionStatusCode.Success;
-
- ///
- /// Gets the endpoints.
- ///
- public ServiceEndPointCollection? EndPoints { get; } = endPoints;
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs
similarity index 55%
rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs
rename to src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs
index c9540f2e7a..807981226e 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs
@@ -1,47 +1,40 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Collections;
using System.Diagnostics;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Primitives;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery;
///
-/// Represents an immutable collection of service endpoints.
+/// Represents a collection of service endpoints.
///
[DebuggerDisplay("{ToString(),nq}")]
[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))]
-public class ServiceEndPointCollection : IReadOnlyList
+public sealed class ServiceEndPointSource
{
private readonly List? _endpoints;
///
- /// Initializes a new instance.
+ /// Initializes a new instance.
///
- /// The service name.
/// The endpoints.
/// The change token.
/// The feature collection.
- public ServiceEndPointCollection(string serviceName, List? endpoints, IChangeToken changeToken, IFeatureCollection features)
+ public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features)
{
- ArgumentNullException.ThrowIfNull(serviceName);
ArgumentNullException.ThrowIfNull(changeToken);
_endpoints = endpoints;
Features = features;
- ServiceName = serviceName;
ChangeToken = changeToken;
}
- ///
- public ServiceEndPoint this[int index] => _endpoints?[index] ?? throw new ArgumentOutOfRangeException(nameof(index));
-
///
- /// Gets the service name.
+ /// Gets the endpoints.
///
- public string ServiceName { get; }
+ public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[];
///
/// Gets the change token which indicates when this collection should be refreshed.
@@ -53,15 +46,6 @@ public ServiceEndPointCollection(string serviceName, List? endp
///
public IFeatureCollection Features { get; }
- ///
- public int Count => _endpoints?.Count ?? 0;
-
- ///
- public IEnumerator GetEnumerator() => _endpoints?.GetEnumerator() ?? Enumerable.Empty().GetEnumerator();
-
- ///
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
-
///
public override string ToString()
{
@@ -73,15 +57,13 @@ public override string ToString()
return $"[{string.Join(", ", eps)}]";
}
- private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value)
+ private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value)
{
- public string ServiceName => value.ServiceName;
-
public IChangeToken ChangeToken => value.ChangeToken;
public IFeatureCollection Features => value.Features;
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
- public ServiceEndPoint[] EndPoints => value.ToArray();
+ public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray();
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs
index 4a8350483e..a2601c84b4 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs
@@ -4,7 +4,6 @@
using System.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
@@ -45,8 +44,7 @@ protected override async Task ResolveAsyncCore()
if (endPoints.Count == 0)
{
- SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."));
- return;
+ throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).");
}
SetResult(endPoints, ttl);
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs
index 516c8ed1f6..9d6c54e475 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs
@@ -3,7 +3,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
@@ -18,7 +17,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin
private readonly TimeProvider _timeProvider;
private long _lastRefreshTimeStamp;
private Task _resolveTask = Task.CompletedTask;
- private ResolutionStatus _lastStatus;
+ private bool _hasEndpoints;
private CancellationChangeToken _lastChangeToken;
private CancellationTokenSource _lastCollectionCancellation;
private List? _lastEndPointCollection;
@@ -59,13 +58,13 @@ protected DnsServiceEndPointResolverBase(
protected CancellationToken ShutdownToken => _disposeCancellation.Token;
///
- public async ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken)
+ public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken)
{
// Only add endpoints to the collection if a previous provider (eg, a configuration override) did not add them.
if (endPoints.EndPoints.Count != 0)
{
Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints");
- return ResolutionStatus.None;
+ return;
}
if (ShouldRefresh())
@@ -75,7 +74,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS
{
if (_resolveTask.IsCompleted && ShouldRefresh())
{
- _resolveTask = ResolveAsyncInternal();
+ _resolveTask = ResolveAsyncCore();
}
resolveTask = _resolveTask;
@@ -95,7 +94,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS
}
endPoints.AddChangeToken(_lastChangeToken);
- return _lastStatus;
+ return;
}
}
@@ -103,53 +102,21 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS
protected abstract Task ResolveAsyncCore();
- private async Task ResolveAsyncInternal()
- {
- try
- {
- await ResolveAsyncCore().ConfigureAwait(false);
- }
- catch (Exception exception)
- {
- SetException(exception);
- throw;
- }
-
- }
-
- protected void SetException(Exception exception) => SetResult(endPoints: null, exception, validityPeriod: TimeSpan.Zero);
-
- protected void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod);
-
- private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod)
+ protected void SetResult(List endPoints, TimeSpan validityPeriod)
{
lock (_lock)
{
- if (exception is not null)
+ if (endPoints is { Count: > 0 })
{
- _nextRefreshPeriod = GetRefreshPeriod();
- if (_lastEndPointCollection is null)
- {
- // Since end points have never been resolved, use a pending status to indicate that they might appear
- // soon and to retry for some period until they do.
- _lastStatus = ResolutionStatus.FromPending(exception);
- }
- else
- {
- _lastStatus = ResolutionStatus.FromException(exception);
- }
+ _lastRefreshTimeStamp = _timeProvider.GetTimestamp();
+ _nextRefreshPeriod = DefaultRefreshPeriod;
+ _hasEndpoints = true;
}
- else if (endPoints is not { Count: > 0 })
+ else
{
_nextRefreshPeriod = GetRefreshPeriod();
validityPeriod = TimeSpan.Zero;
- _lastStatus = ResolutionStatus.Pending;
- }
- else
- {
- _lastRefreshTimeStamp = _timeProvider.GetTimestamp();
- _nextRefreshPeriod = DefaultRefreshPeriod;
- _lastStatus = ResolutionStatus.Success;
+ _hasEndpoints = false;
}
if (validityPeriod <= TimeSpan.Zero)
@@ -169,13 +136,18 @@ private void SetResult(List? endPoints, Exception? exception, T
TimeSpan GetRefreshPeriod()
{
- if (_lastStatus.StatusCode is ResolutionStatusCode.Success)
+ if (_hasEndpoints)
{
return MinRetryPeriod;
}
- var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor));
- return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod;
+ var nextTicks = (long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor);
+ if (nextTicks <= 0 || nextTicks > MaxRetryPeriod.Ticks)
+ {
+ return MaxRetryPeriod;
+ }
+
+ return TimeSpan.FromTicks(nextTicks);
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs
index 37879b5f3e..665c98bbc0 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
///
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs
index 8f676327d5..51525663a0 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs
@@ -4,28 +4,18 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
-using Microsoft.Extensions.ServiceDiscovery.Internal;
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
internal sealed partial class DnsServiceEndPointResolverProvider(
IOptionsMonitor options,
ILogger logger,
- TimeProvider timeProvider,
- ServiceNameParser parser) : IServiceEndPointResolverProvider
+ TimeProvider timeProvider) : IServiceEndPointProviderFactory
{
///
- public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
+ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
- if (!parser.TryParse(serviceName, out var parts))
- {
- DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName);
- resolver = default;
- return false;
- }
-
- resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider);
+ resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider);
return true;
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs
index 97a0d47d02..d59dbfbb69 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs
@@ -6,7 +6,6 @@
using DnsClient.Protocol;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
@@ -39,8 +38,7 @@ protected override async Task ResolveAsyncCore()
var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false);
if (result.HasError)
{
- SetException(CreateException(srvQuery, result.ErrorMessage));
- return;
+ throw CreateException(srvQuery, result.ErrorMessage);
}
var lookupMapping = new Dictionary();
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs
index 5bac96c6c0..704e03cd9c 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
///
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs
index d02dcfb727..8a75c1d1bb 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs
@@ -5,8 +5,6 @@
using DnsClient;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
-using Microsoft.Extensions.ServiceDiscovery.Internal;
namespace Microsoft.Extensions.ServiceDiscovery.Dns;
@@ -14,8 +12,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider(
IOptionsMonitor options,
ILogger logger,
IDnsQuery dnsClient,
- TimeProvider timeProvider,
- ServiceNameParser parser) : IServiceEndPointResolverProvider
+ TimeProvider timeProvider) : IServiceEndPointProviderFactory
{
private static readonly string s_serviceAccountPath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount");
private static readonly string s_serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace");
@@ -23,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider(
private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain();
///
- public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
+ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
// If a default namespace is not specified, then this provider will attempt to infer the namespace from the service name, but only when running inside Kubernetes.
// Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md
@@ -33,6 +30,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi
// Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local".
// The protocol is assumed to be "tcp".
// The portName is the name of the port in the service definition. If the serviceName parses as a URI, we use the scheme as the port name, otherwise "default".
+ var serviceName = query.OriginalString;
if (string.IsNullOrWhiteSpace(_querySuffix))
{
DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName);
@@ -40,16 +38,9 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi
return false;
}
- if (!parser.TryParse(serviceName, out var parts))
- {
- DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName);
- resolver = default;
- return false;
- }
-
- var portName = parts.EndPointName ?? "default";
- var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}";
- resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider);
+ var portName = query.EndPointName ?? "default";
+ var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}";
+ resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider);
return true;
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs
similarity index 88%
rename from src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs
rename to src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs
index e385bde69a..0d795660fd 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs
@@ -4,7 +4,7 @@
using DnsClient;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery;
using Microsoft.Extensions.ServiceDiscovery.Dns;
namespace Microsoft.Extensions.Hosting;
@@ -12,7 +12,7 @@ namespace Microsoft.Extensions.Hosting;
///
/// Extensions for to add service discovery.
///
-public static class HostingExtensions
+public static class ServiceDiscoveryDnsServiceCollectionExtensions
{
///
/// Adds DNS SRV service discovery to the .
@@ -28,7 +28,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC
{
services.AddServiceDiscoveryCore();
services.TryAddSingleton();
- services.AddSingleton();
+ services.AddSingleton();
var options = services.AddOptions();
options.Configure(o => configureOptions?.Invoke(o));
return services;
@@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC
public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null)
{
services.AddServiceDiscoveryCore();
- services.AddSingleton();
+ services.AddSingleton();
var options = services.AddOptions();
options.Configure(o => configureOptions?.Invoke(o));
return services;
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs
index e35be5d629..22d7e6d832 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs
@@ -54,32 +54,32 @@ public async ValueTask ResolveDestinationsAsync(I
var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority;
var serviceName = originalUri.GetLeftPart(UriPartial.Authority);
- var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false);
- var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count);
+ var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false);
+ var results = new List<(string Name, DestinationConfig Config)>(result.EndPoints.Count);
var uriBuilder = new UriBuilder(originalUri);
var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null;
var healthUriBuilder = healthUri is { } ? new UriBuilder(healthUri) : null;
- foreach (var endPoint in endPoints)
+ foreach (var endPoint in result.EndPoints)
{
var addressString = endPoint.GetEndPointString();
- Uri result;
+ Uri uri;
if (!addressString.Contains("://"))
{
- result = new Uri($"https://{addressString}");
+ uri = new Uri($"https://{addressString}");
}
else
{
- result = new Uri(addressString);
+ uri = new Uri(addressString);
}
- uriBuilder.Host = result.Host;
- uriBuilder.Port = result.Port;
+ uriBuilder.Host = uri.Host;
+ uriBuilder.Port = uri.Port;
var resolvedAddress = uriBuilder.Uri.ToString();
var healthAddress = originalConfig.Health;
if (healthUriBuilder is not null)
{
- healthUriBuilder.Host = result.Host;
- healthUriBuilder.Port = result.Port;
+ healthUriBuilder.Host = uri.Host;
+ healthUriBuilder.Port = uri.Port;
healthAddress = healthUriBuilder.Uri.ToString();
}
@@ -88,6 +88,6 @@ public async ValueTask ResolveDestinationsAsync(I
results.Add((name, config));
}
- return (results, endPoints.ChangeToken);
+ return (results, result.ChangeToken);
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs
index d37e4f1407..84aafe2a67 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs
@@ -1,22 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
using Microsoft.Extensions.ServiceDiscovery.Http;
using Yarp.ReverseProxy.Forwarder;
namespace Microsoft.Extensions.ServiceDiscovery.Yarp;
-internal sealed class ServiceDiscoveryForwarderHttpClientFactory(
- TimeProvider timeProvider,
- IServiceEndPointSelectorProvider selectorProvider,
- ServiceEndPointResolverFactory factory,
- IOptions options) : ForwarderHttpClientFactory
+internal sealed class ServiceDiscoveryForwarderHttpClientFactory(IServiceDiscoveryHttpMessageHandlerFactory handlerFactory)
+ : ForwarderHttpClientFactory
{
protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler)
{
- var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider);
- return new ResolvingHttpDelegatingHandler(registry, options, handler);
+ return handlerFactory.CreateHandler(handler);
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs
similarity index 94%
rename from src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs
rename to src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs
index e52ff65ee2..9f473fd3a9 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.ServiceDiscovery.Yarp;
using Yarp.ReverseProxy.Forwarder;
using Yarp.ReverseProxy.ServiceDiscovery;
@@ -11,7 +10,7 @@ namespace Microsoft.Extensions.DependencyInjection;
///
/// Extensions for used to register the ReverseProxy's components.
///
-public static class ReverseProxyServiceCollectionExtensions
+public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions
{
///
/// Provides a implementation which uses service discovery to resolve destinations.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs
index 5916951c69..fdb61ef59f 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs
@@ -1,12 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Globalization;
using System.Text;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery.Internal;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.Configuration;
internal sealed partial class ConfigurationServiceEndPointResolver
{
@@ -15,38 +13,16 @@ private sealed partial class Log
[LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")]
public static partial void SkippedResolution(ILogger logger, string serviceName, string reason);
- [LoggerMessage(2, LogLevel.Debug, "Matching endpoints using endpoint names for service '{ServiceName}' since endpoint names are specified in configuration.", EventName = "MatchingEndPointNames")]
- public static partial void MatchingEndPointNames(ILogger logger, string serviceName);
-
- [LoggerMessage(3, LogLevel.Debug, "Ignoring endpoints using endpoint names for service '{ServiceName}' since no endpoint names are specified in configuration.", EventName = "IgnoringEndPointNames")]
- public static partial void IgnoringEndPointNames(ILogger logger, string serviceName);
-
- public static void EndPointNameMatchSelection(ILogger logger, string serviceName, bool matchEndPointNames)
- {
- if (!logger.IsEnabled(LogLevel.Debug))
- {
- return;
- }
-
- if (matchEndPointNames)
- {
- MatchingEndPointNames(logger, serviceName);
- }
- else
- {
- IgnoringEndPointNames(logger, serviceName);
- }
- }
-
- [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")]
+ [LoggerMessage(2, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoint '{EndpointName}' for service '{ServiceName}'.", EventName = "UsingConfigurationPath")]
public static partial void UsingConfigurationPath(ILogger logger, string path, string endpointName, string serviceName);
- [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")]
+ [LoggerMessage(3, LogLevel.Debug, "No valid endpoint configuration was found for service '{ServiceName}' from path '{Path}'.", EventName = "ServiceConfigurationNotFound")]
internal static partial void ServiceConfigurationNotFound(ILogger logger, string serviceName, string path);
- [LoggerMessage(6, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")]
+ [LoggerMessage(4, LogLevel.Debug, "Endpoints configured for service '{ServiceName}' from path '{Path}': {ConfiguredEndPoints}.", EventName = "ConfiguredEndPoints")]
internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints);
- public static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, List parsedValues)
+
+ internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added)
{
if (!logger.IsEnabled(LogLevel.Debug))
{
@@ -54,21 +30,21 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin
}
StringBuilder endpointValues = new();
- for (var i = 0; i < parsedValues.Count; i++)
+ for (var i = endpoints.Count - added; i < endpoints.Count; i++)
{
if (endpointValues.Length > 0)
{
endpointValues.Append(", ");
}
- endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})");
+ endpointValues.Append(endpoints[i].ToString());
}
var configuredEndPoints = endpointValues.ToString();
ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints);
}
- [LoggerMessage(7, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")]
+ [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")]
internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path);
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs
index dae054c988..9604ec0c20 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs
@@ -6,9 +6,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Internal;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.Configuration;
///
/// A service endpoint resolver that uses configuration to resolve resolved.
@@ -26,29 +25,21 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd
///
/// Initializes a new instance.
///
- /// The service name.
+ /// The query.
/// The configuration.
/// The logger.
- /// The options.
- /// The service name parser.
+ /// Configuration resolver options.
+ /// Service discovery options.
public ConfigurationServiceEndPointResolver(
- string serviceName,
+ ServiceEndPointQuery query,
IConfiguration configuration,
ILogger logger,
IOptions options,
- ServiceNameParser parser)
+ IOptions serviceDiscoveryOptions)
{
- if (parser.TryParse(serviceName, out var parts))
- {
- _serviceName = parts.Host;
- _endpointName = parts.EndPointName;
- _schemes = parts.Schemes;
- }
- else
- {
- throw new InvalidOperationException($"Service name '{serviceName}' is not valid.");
- }
-
+ _serviceName = query.ServiceName;
+ _endpointName = query.EndPointName;
+ _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes);
_configuration = configuration;
_logger = logger;
_options = options;
@@ -58,24 +49,22 @@ public ConfigurationServiceEndPointResolver(
public ValueTask DisposeAsync() => default;
///
- public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints));
-
- string IHostNameFeature.HostName => _serviceName;
-
- private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints)
+ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken)
{
// Only add resolved to the collection if a previous provider (eg, an override) did not add them.
if (endPoints.EndPoints.Count != 0)
{
Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints");
- return ResolutionStatus.None;
+ return default;
}
// Get the corresponding config section.
var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName);
if (!section.Exists())
{
- return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}");
+ endPoints.AddChangeToken(_configuration.GetReloadToken());
+ Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}");
+ return default;
}
endPoints.AddChangeToken(section.GetReloadToken());
@@ -119,7 +108,8 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin
var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}";
if (!namedSection.Exists())
{
- return CreateNotFoundResponse(endPoints, configPath);
+ Log.EndpointConfigurationNotFound(_logger, endpointName, _serviceName, configPath);
+ return default;
}
List resolved = [];
@@ -129,10 +119,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin
if (!string.IsNullOrWhiteSpace(namedSection.Value))
{
// Single value case.
- if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error))
- {
- return error;
- }
+ AddEndPoint(resolved, namedSection, endpointName);
}
else
{
@@ -141,13 +128,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin
{
if (!int.TryParse(child.Key, out _))
{
- return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."));
+ throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.");
}
- if (!TryAddEndPoint(resolved, child, endpointName, out var error))
- {
- return error;
- }
+ AddEndPoint(resolved, child, endpointName);
}
}
@@ -186,25 +170,27 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin
if (added == 0)
{
- return CreateNotFoundResponse(endPoints, configPath);
+ Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath);
+ }
+ else
+ {
+ Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added);
}
- return ResolutionStatus.Success;
-
+ return default;
}
- private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error)
+ string IHostNameFeature.HostName => _serviceName;
+
+ private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName)
{
var value = section.Value;
if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint))
{
- error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."));
- return false;
+ throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.");
}
endPoints.Add(CreateEndPoint(endPoint));
- error = default;
- return true;
}
private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint)
@@ -246,12 +232,5 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint)
return serviceEndPoint;
}
- private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath)
- {
- endPoints.AddChangeToken(_configuration.GetReloadToken());
- Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath);
- return ResolutionStatus.CreateNotFound($"No valid endpoint configuration was found for service '{_serviceName}' from path '{configPath}'.");
- }
-
public override string ToString() => "Configuration";
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs
similarity index 50%
rename from src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs
index c83589eb26..91e97b5d0b 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs
@@ -1,25 +1,9 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Options;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Options for .
-///
-public sealed class ConfigurationServiceEndPointResolverOptions
-{
- ///
- /// The name of the configuration section which contains service endpoints. Defaults to "Services".
- ///
- public string SectionName { get; set; } = "Services";
-
- ///
- /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false.
- ///
- public Func ApplyHostNameMetadata { get; set; } = _ => false;
-}
+namespace Microsoft.Extensions.ServiceDiscovery.Configuration;
internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions
{
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs
index 472205f12f..032c50b6f2 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs
@@ -5,23 +5,22 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Internal;
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.Configuration;
///
-/// implementation that resolves services using .
+/// implementation that resolves services using .
///
internal sealed class ConfigurationServiceEndPointResolverProvider(
IConfiguration configuration,
IOptions options,
- ILogger logger,
- ServiceNameParser parser) : IServiceEndPointResolverProvider
+ IOptions serviceDiscoveryOptions,
+ ILogger logger) : IServiceEndPointProviderFactory
{
///
- public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
+ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
- resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser);
+ resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions);
return true;
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs
new file mode 100644
index 0000000000..d3b94f2f1e
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.ServiceDiscovery.Configuration;
+
+namespace Microsoft.Extensions.ServiceDiscovery;
+
+///
+/// Options for .
+///
+public sealed class ConfigurationServiceEndPointResolverOptions
+{
+ ///
+ /// The name of the configuration section which contains service endpoints. Defaults to "Services".
+ ///
+ public string SectionName { get; set; } = "Services";
+
+ ///
+ /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to a delegate which returns false.
+ ///
+ public Func ApplyHostNameMetadata { get; set; } = _ => false;
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs
index b4f6249f28..44e58b0dbb 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs
@@ -3,21 +3,21 @@
using System.Collections.Concurrent;
using System.Diagnostics;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.ServiceDiscovery.LoadBalancing;
namespace Microsoft.Extensions.ServiceDiscovery.Http;
///
/// Resolves endpoints for HTTP requests.
///
-public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable
+internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable
{
private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers();
private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10);
private readonly object _lock = new();
- private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory;
- private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider;
+ private readonly ServiceEndPointWatcherFactory _resolverFactory = resolverFactory;
private readonly ConcurrentDictionary _resolvers = new();
private ITimer? _cleanupTimer;
private Task? _cleanupTask;
@@ -148,8 +148,8 @@ private async Task CleanupResolversAsyncCore()
private ResolverEntry CreateResolver(string serviceName)
{
- var resolver = _resolverFactory.CreateResolver(serviceName);
- var selector = _selectorProvider.CreateSelector();
+ var resolver = _resolverFactory.CreateWatcher(serviceName);
+ var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector();
var result = new ResolverEntry(resolver, selector);
resolver.Start();
return result;
@@ -173,7 +173,7 @@ public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector s
{
if (result.ResolvedSuccessfully)
{
- _selector.SetEndPoints(result.EndPoints);
+ _selector.SetEndPoints(result.EndPointSource);
}
};
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs
new file mode 100644
index 0000000000..0febfa9481
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.ServiceDiscovery.Http;
+
+///
+/// Factory which creates instances which resolve endpoints using service discovery
+/// before delegating to a provided handler.
+///
+public interface IServiceDiscoveryHttpMessageHandlerFactory
+{
+ ///
+ /// Creates an instance which resolve endpoints using service discovery before
+ /// delegating to a provided handler.
+ ///
+ /// The handler to delegate to.
+ /// The new .
+ HttpMessageHandler CreateHandler(HttpMessageHandler handler);
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs
index 39eb65cc18..bc06a03170 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs
@@ -1,16 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.Http;
///
/// which resolves endpoints using service discovery.
///
-public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler
+internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler
{
private readonly HttpServiceEndPointResolver _resolver = resolver;
private readonly ServiceDiscoveryOptions _options = options.Value;
@@ -19,29 +17,20 @@ public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IO
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var originalUri = request.RequestUri;
- IEndPointHealthFeature? epHealth = null;
- Exception? error = null;
- var startTime = Stopwatch.GetTimestamp();
- if (originalUri?.Host is not null)
- {
- var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false);
- request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options);
- request.Headers.Host ??= result.Features.Get()?.HostName;
- epHealth = result.Features.Get();
- }
try
{
+ if (originalUri?.Host is not null)
+ {
+ var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false);
+ request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options);
+ request.Headers.Host ??= result.Features.Get()?.HostName;
+ }
+
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
- catch (Exception exception)
- {
- error = exception;
- throw;
- }
finally
{
- epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?.
request.RequestUri = originalUri;
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs
index 976bfb331e..daa7b8a17d 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs
@@ -1,17 +1,15 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.Diagnostics;
using System.Net;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.Http;
///
/// HTTP message handler which resolves endpoints using service discovery.
///
-public class ResolvingHttpDelegatingHandler : DelegatingHandler
+internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler
{
private readonly HttpServiceEndPointResolver _resolver;
private readonly ServiceDiscoveryOptions _options;
@@ -43,29 +41,19 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var originalUri = request.RequestUri;
- IEndPointHealthFeature? epHealth = null;
- Exception? error = null;
- var startTime = Stopwatch.GetTimestamp();
if (originalUri?.Host is not null)
{
var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false);
request.RequestUri = GetUriWithEndPoint(originalUri, result, _options);
request.Headers.Host ??= result.Features.Get()?.HostName;
- epHealth = result.Features.Get();
}
try
{
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
- catch (Exception exception)
- {
- error = exception;
- throw;
- }
finally
{
- epHealth?.ReportHealth(Stopwatch.GetElapsedTime(startTime), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?.
request.RequestUri = originalUri;
}
}
@@ -124,7 +112,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint,
if (uri.Scheme.IndexOf('+') > 0)
{
var scheme = uri.Scheme.Split('+')[0];
- if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase))
+ if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase))
{
result.Scheme = scheme;
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs
new file mode 100644
index 0000000000..0d3ba00122
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs
@@ -0,0 +1,19 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.ServiceDiscovery.Http;
+
+internal sealed class ServiceDiscoveryHttpMessageHandlerFactory(
+ TimeProvider timeProvider,
+ IServiceProvider serviceProvider,
+ ServiceEndPointWatcherFactory factory,
+ IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory
+{
+ public HttpMessageHandler CreateHandler(HttpMessageHandler handler)
+ {
+ var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider);
+ return new ResolvingHttpDelegatingHandler(registry, options, handler);
+ }
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs
new file mode 100644
index 0000000000..07bffa5654
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace Microsoft.Extensions.ServiceDiscovery.Internal;
+
+///
+/// Represents the result of service endpoint resolution.
+///
+/// The endpoint collection.
+/// The exception which occurred during resolution.
+internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception)
+{
+ ///
+ /// Gets the exception which occurred during resolution.
+ ///
+ public Exception? Exception { get; } = exception;
+
+ ///
+ /// Gets a value indicating whether resolution completed successfully.
+ ///
+ [MemberNotNullWhen(true, nameof(EndPointSource))]
+ public bool ResolvedSuccessfully => Exception is null;
+
+ ///
+ /// Gets the endpoints.
+ ///
+ public ServiceEndPointSource? EndPointSource { get; } = endPointSource;
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs
deleted file mode 100644
index de04748187..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using System.Diagnostics.CodeAnalysis;
-using Microsoft.Extensions.Options;
-
-namespace Microsoft.Extensions.ServiceDiscovery.Internal;
-
-internal sealed class ServiceNameParser(IOptions options)
-{
- private readonly string[] _allowedSchemes = options.Value.AllowedSchemes;
-
- public bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts)
- {
- if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri))
- {
- parts = Create(uri, hasScheme: false);
- return true;
- }
-
- if (Uri.TryCreate(serviceName, default, out uri))
- {
- parts = Create(uri, hasScheme: true);
- return true;
- }
-
- parts = default;
- return false;
-
- ServiceNameParts Create(Uri uri, bool hasScheme)
- {
- var uriHost = uri.Host;
- var segmentSeparatorIndex = uriHost.IndexOf('.');
- string host;
- string? endPointName = null;
- var port = uri.Port > 0 ? uri.Port : 0;
- if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.')
- {
- endPointName = uriHost[1..segmentSeparatorIndex];
-
- // Skip the endpoint name, including its prefix ('_') and suffix ('.').
- host = uriHost[(segmentSeparatorIndex + 1)..];
- }
- else
- {
- host = uriHost;
- }
-
- // Allow multiple schemes to be separated by a '+', eg. "https+http://host:port".
- var schemes = hasScheme ? ParseSchemes(uri.Scheme) : [];
- return new(schemes, host, endPointName, port);
- }
- }
-
- private string[] ParseSchemes(string scheme)
- {
- if (_allowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes))
- {
- return scheme.Split('+');
- }
-
- List result = [];
- foreach (var s in scheme.Split('+'))
- {
- foreach (var allowed in _allowedSchemes)
- {
- if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase))
- {
- result.Add(s);
- break;
- }
- }
- }
-
- return result.ToArray();
- }
-}
-
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs
deleted file mode 100644
index f93729a40e..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Internal;
-
-internal readonly struct ServiceNameParts : IEquatable
-{
- public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this()
- {
- Schemes = schemePriority;
- Host = host;
- EndPointName = endPointName;
- Port = port;
- }
-
- public string? EndPointName { get; init; }
-
- public string[] Schemes { get; init; }
-
- public string Host { get; init; }
-
- public int Port { get; init; }
-
- public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}";
-
- public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other);
-
- public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port);
-
- public bool Equals(ServiceNameParts other) =>
- EndPointName == other.EndPointName &&
- Host == other.Host &&
- Port == other.Port;
-
- public static bool operator ==(ServiceNameParts left, ServiceNameParts right) => left.Equals(right);
-
- public static bool operator !=(ServiceNameParts left, ServiceNameParts right) => !(left == right);
-}
-
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs
similarity index 73%
rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs
index e2ffbd0421..bd0172c45c 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs
@@ -1,21 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing;
///
/// Selects endpoints from a collection of endpoints.
///
-public interface IServiceEndPointSelector
+internal interface IServiceEndPointSelector
{
///
/// Sets the collection of endpoints which this instance will select from.
///
/// The collection of endpoints to select from.
- void SetEndPoints(ServiceEndPointCollection endPoints);
+ void SetEndPoints(ServiceEndPointSource endPoints);
///
- /// Selects an endpoints from the collection provided by the most recent call to .
+ /// Selects an endpoints from the collection provided by the most recent call to .
///
/// The context.
/// An endpoint.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs
deleted file mode 100644
index 9395896e52..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// A service endpoint selector which always returns the first endpoint in a collection.
-///
-public class PickFirstServiceEndPointSelector : IServiceEndPointSelector
-{
- private ServiceEndPointCollection? _endPoints;
-
- ///
- public ServiceEndPoint GetEndPoint(object? context)
- {
- if (_endPoints is not { Count: > 0 } endPoints)
- {
- throw new InvalidOperationException("The endpoint collection contains no endpoints");
- }
-
- return endPoints[0];
- }
-
- ///
- public void SetEndPoints(ServiceEndPointCollection endPoints)
- {
- _endPoints = endPoints;
- }
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs
deleted file mode 100644
index d3f657c955..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Provides instances of .
-///
-public class PickFirstServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider
-{
- ///
- /// Gets a shared instance of this class.
- ///
- public static PickFirstServiceEndPointSelectorProvider Instance { get; } = new();
-
- ///
- public IServiceEndPointSelector CreateSelector() => new PickFirstServiceEndPointSelector();
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs
deleted file mode 100644
index e233dfb7b5..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Selects endpoints using the Power of Two Choices algorithm for distributed load balancing based on
-/// the last-known load of the candidate endpoints.
-///
-public class PowerOfTwoChoicesServiceEndPointSelector : IServiceEndPointSelector
-{
- private ServiceEndPointCollection? _endPoints;
-
- ///
- public void SetEndPoints(ServiceEndPointCollection endPoints)
- {
- _endPoints = endPoints;
- }
-
- ///
- public ServiceEndPoint GetEndPoint(object? context)
- {
- if (_endPoints is not { Count: > 0 } collection)
- {
- throw new InvalidOperationException("The endpoint collection contains no endpoints");
- }
-
- if (collection.Count == 1)
- {
- return collection[0];
- }
-
- var first = collection[Random.Shared.Next(collection.Count)];
- ServiceEndPoint second;
- do
- {
- second = collection[Random.Shared.Next(collection.Count)];
- } while (ReferenceEquals(first, second));
-
- // Note that this relies on fresh data to be effective.
- if (first.Features.Get() is { } firstLoad
- && second.Features.Get() is { } secondLoad)
- {
- return firstLoad.CurrentLoad < secondLoad.CurrentLoad ? first : second;
- }
-
- // Degrade to random.
- return first;
- }
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs
deleted file mode 100644
index 00832bc781..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Provides instances of .
-///
-public class PowerOfTwoChoicesServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider
-{
- ///
- /// Gets a shared instance of this class.
- ///
- public static PowerOfTwoChoicesServiceEndPointSelectorProvider Instance { get; } = new();
-
- ///
- public IServiceEndPointSelector CreateSelector() => new PowerOfTwoChoicesServiceEndPointSelector();
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs
deleted file mode 100644
index 8e4bb2378d..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// A service endpoint selector which returns random endpoints from the collection.
-///
-public class RandomServiceEndPointSelector : IServiceEndPointSelector
-{
- private ServiceEndPointCollection? _endPoints;
-
- ///
- public void SetEndPoints(ServiceEndPointCollection endPoints)
- {
- _endPoints = endPoints;
- }
-
- ///
- public ServiceEndPoint GetEndPoint(object? context)
- {
- if (_endPoints is not { Count: > 0 } collection)
- {
- throw new InvalidOperationException("The endpoint collection contains no endpoints");
- }
-
- return collection[Random.Shared.Next(collection.Count)];
- }
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs
deleted file mode 100644
index ae74b4032b..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Provides instances of .
-///
-public class RandomServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider
-{
- ///
- /// Gets a shared instance of this class.
- ///
- public static RandomServiceEndPointSelectorProvider Instance { get; } = new();
-
- ///
- public IServiceEndPointSelector CreateSelector() => new RandomServiceEndPointSelector();
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs
index 5848c7d8f7..92da7cf25b 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs
@@ -1,20 +1,20 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
+namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing;
///
/// Selects endpoints by iterating through the list of endpoints in a round-robin fashion.
///
-public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector
+internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector
{
private uint _next;
- private ServiceEndPointCollection? _endPoints;
+ private IReadOnlyList? _endPoints;
///
- public void SetEndPoints(ServiceEndPointCollection endPoints)
+ public void SetEndPoints(ServiceEndPointSource endPoints)
{
- _endPoints = endPoints;
+ _endPoints = endPoints.EndPoints;
}
///
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs
deleted file mode 100644
index 40d9ce7845..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Provides instances of .
-///
-public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelectorProvider
-{
- ///
- /// Gets a shared instance of this class.
- ///
- public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new();
-
- ///
- public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector();
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj
index 6836e58cf6..9a5d67db04 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj
+++ b/src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj
@@ -10,7 +10,8 @@
-
+
+
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs
index ab0ea286b6..483c08702d 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs
@@ -3,7 +3,6 @@
using System.Net;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.PassThrough;
@@ -12,18 +11,17 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough;
///
internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider
{
- public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken)
+ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken)
{
- if (endPoints.EndPoints.Count != 0)
+ if (endPoints.EndPoints.Count == 0)
{
- return new(ResolutionStatus.None);
+ Log.UsingPassThrough(logger, serviceName);
+ var ep = ServiceEndPoint.Create(endPoint);
+ ep.Features.Set(this);
+ endPoints.EndPoints.Add(ep);
}
- Log.UsingPassThrough(logger, serviceName);
- var ep = ServiceEndPoint.Create(endPoint);
- ep.Features.Set(this);
- endPoints.EndPoints.Add(ep);
- return new(ResolutionStatus.Success);
+ return default;
}
public ValueTask DisposeAsync() => default;
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs
index b3a326010b..83455e0979 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs
@@ -4,18 +4,18 @@
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery.PassThrough;
///
/// Service endpoint resolver provider which passes through the provided value.
///
-internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider
+internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory
{
///
- public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
+ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
+ var serviceName = query.OriginalString;
if (!TryCreateEndPoint(serviceName, out var endPoint))
{
// Propagate the value through regardless, leaving it to the caller to interpret it.
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
similarity index 73%
rename from src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
index c1c833de89..bcfa59056c 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs
@@ -2,11 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.ServiceDiscovery;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
using Microsoft.Extensions.ServiceDiscovery.Http;
namespace Microsoft.Extensions.DependencyInjection;
@@ -14,49 +12,30 @@ namespace Microsoft.Extensions.DependencyInjection;
///
/// Extensions for configuring with service discovery.
///
-public static class HttpClientBuilderExtensions
+public static class ServiceDiscoveryHttpClientBuilderExtensions
{
///
/// Adds service discovery to the .
///
/// The builder.
- /// The provider that creates selector instances.
/// The builder.
- public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder, IServiceEndPointSelectorProvider selectorProvider)
- {
- var services = httpClientBuilder.Services;
- services.AddServiceDiscoveryCore();
- httpClientBuilder.AddHttpMessageHandler(services =>
- {
- var timeProvider = services.GetService() ?? TimeProvider.System;
- var resolverProvider = services.GetRequiredService();
- var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider);
- var options = services.GetRequiredService>();
- return new ResolvingHttpDelegatingHandler(registry, options);
- });
-
- // Configure the HttpClient to disable gRPC load balancing.
- // This is done on all HttpClient instances but only impacts gRPC clients.
- AddDisableGrpcLoadBalancingFilter(httpClientBuilder.Services, httpClientBuilder.Name);
-
- return httpClientBuilder;
- }
+ [Obsolete(error: true, message: "This method is obsolete and will be removed in a future version. The recommended alternative is to use the 'AddServiceDiscovery' method instead.")]
+ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder) => httpClientBuilder.AddServiceDiscovery();
///
/// Adds service discovery to the .
///
/// The builder.
/// The builder.
- public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder httpClientBuilder)
+ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder)
{
var services = httpClientBuilder.Services;
services.AddServiceDiscoveryCore();
httpClientBuilder.AddHttpMessageHandler(services =>
{
var timeProvider = services.GetService() ?? TimeProvider.System;
- var selectorProvider = services.GetRequiredService();
- var resolverProvider = services.GetRequiredService();
- var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider);
+ var resolverProvider = services.GetRequiredService();
+ var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider);
var options = services.GetRequiredService>();
return new ResolvingHttpDelegatingHandler(registry, options);
});
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs
index d9510a3cf2..89c5a2d2eb 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs
@@ -1,29 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using Microsoft.Extensions.Primitives;
+
namespace Microsoft.Extensions.ServiceDiscovery;
///
-/// Options for configuring service discovery.
+/// Options for service endpoint resolvers.
///
public sealed class ServiceDiscoveryOptions
{
///
- /// The value for which indicates that all schemes are allowed.
+ /// The value indicating that all endpoint schemes are allowed.
///
#pragma warning disable IDE0300 // Simplify collection initialization
#pragma warning disable CA1825 // Avoid zero-length array allocations
- public static readonly string[] AllSchemes = new string[0];
+ public static readonly string[] AllowAllSchemes = new string[0];
#pragma warning restore CA1825 // Avoid zero-length array allocations
#pragma warning restore IDE0300 // Simplify collection initialization
+ ///
+ /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via .
+ ///
+ public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60);
+
///
/// Gets or sets the collection of allowed URI schemes for URIs resolved by the service discovery system when multiple schemes are specified, for example "https+http://_endpoint.service".
///
///
- /// When set to , all schemes are allowed.
+ /// When set to , all schemes are allowed.
/// Schemes are not case-sensitive.
///
- public string[] AllowedSchemes { get; set; } = AllSchemes;
-}
+ public string[] AllowedSchemes { get; set; } = AllowAllSchemes;
+ internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes)
+ {
+ if (allowedSchemes.Equals(AllowAllSchemes))
+ {
+ if (schemes is string[] array)
+ {
+ return array;
+ }
+
+ return schemes.ToArray();
+ }
+
+ List result = [];
+ foreach (var s in schemes)
+ {
+ foreach (var allowed in allowedSchemes)
+ {
+ if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase))
+ {
+ result.Add(s);
+ break;
+ }
+ }
+ }
+
+ return result.ToArray();
+ }
+}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs
similarity index 57%
rename from src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs
index a4ba9b63b3..6403b21463 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs
@@ -2,20 +2,21 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.ServiceDiscovery;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery.Configuration;
+using Microsoft.Extensions.ServiceDiscovery.Http;
using Microsoft.Extensions.ServiceDiscovery.Internal;
+using Microsoft.Extensions.ServiceDiscovery.LoadBalancing;
using Microsoft.Extensions.ServiceDiscovery.PassThrough;
-namespace Microsoft.Extensions.Hosting;
+namespace Microsoft.Extensions.DependencyInjection;
///
/// Extension methods for configuring service discovery.
///
-public static class HostingExtensions
+public static class ServiceDiscoveryServiceCollectionExtensions
{
///
/// Adds the core service discovery services and configures defaults.
@@ -29,21 +30,47 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser
.AddPassThroughServiceEndPointResolver();
}
+ ///
+ /// Adds the core service discovery services and configures defaults.
+ ///
+ /// The service collection.
+ /// The delegate used to configure service discovery options.
+ /// The service collection.
+ public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions)
+ {
+ return services.AddServiceDiscoveryCore(configureOptions: configureOptions)
+ .AddConfigurationServiceEndPointResolver()
+ .AddPassThroughServiceEndPointResolver();
+ }
+
///
/// Adds the core service discovery services.
///
/// The service collection.
/// The service collection.
- public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services)
+ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null);
+
+ ///
+ /// Adds the core service discovery services.
+ ///
+ /// The service collection.
+ /// The delegate used to configure service discovery options.
+ /// The service collection.
+ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions)
{
services.AddOptions();
services.AddLogging();
- services.TryAddSingleton();
services.TryAddTransient, ServiceDiscoveryOptionsValidator>();
- services.TryAddSingleton(static sp => TimeProvider.System);
- services.TryAddSingleton();
- services.TryAddSingleton();
- services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService()));
+ services.TryAddSingleton(_ => TimeProvider.System);
+ services.TryAddTransient();
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService()));
+ if (configureOptions is not null)
+ {
+ services.Configure(configureOptions);
+ }
+
return services;
}
@@ -63,10 +90,10 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS
/// The delegate used to configure the provider.
/// The service collection.
/// The service collection.
- public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null)
+ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions)
{
services.AddServiceDiscoveryCore();
- services.AddSingleton();
+ services.AddSingleton();
services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>();
if (configureOptions is not null)
{
@@ -84,7 +111,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS
public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services)
{
services.AddServiceDiscoveryCore();
- services.AddSingleton();
+ services.AddSingleton();
return services;
}
}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs
new file mode 100644
index 0000000000..1a14cb961b
--- /dev/null
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.Extensions.ServiceDiscovery;
+
+///
+/// A mutable collection of service endpoints.
+///
+internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder
+{
+ private readonly List _endPoints = new();
+ private readonly List _changeTokens = new();
+ private readonly FeatureCollection _features = new FeatureCollection();
+
+ ///
+ /// Adds a change token.
+ ///
+ /// The change token.
+ public void AddChangeToken(IChangeToken changeToken)
+ {
+ _changeTokens.Add(changeToken);
+ }
+
+ ///
+ /// Gets the feature collection.
+ ///
+ public IFeatureCollection Features => _features;
+
+ ///
+ /// Gets the endpoints.
+ ///
+ public IList EndPoints => _endPoints;
+
+ ///
+ /// Creates a from the provided instance.
+ ///
+ /// The service endpoint source.
+ public ServiceEndPointSource Build()
+ {
+ return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features);
+ }
+}
+
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs
index 9de4a61b41..029d260124 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs
@@ -3,7 +3,6 @@
using System.Collections.Concurrent;
using System.Diagnostics;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery;
@@ -16,7 +15,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable
private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10);
private readonly object _lock = new();
- private readonly ServiceEndPointResolverFactory _resolverProvider;
+ private readonly ServiceEndPointWatcherFactory _resolverProvider;
private readonly TimeProvider _timeProvider;
private readonly ConcurrentDictionary _resolvers = new();
private ITimer? _cleanupTimer;
@@ -28,7 +27,7 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable
///
/// The resolver factory.
/// The time provider.
- internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider)
+ internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider)
{
_resolverProvider = resolverProvider;
_timeProvider = timeProvider;
@@ -40,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider
/// The service name.
/// A .
/// The resolved service endpoints.
- public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken)
+ public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(serviceName);
ObjectDisposedException.ThrowIf(_disposed, this);
@@ -157,7 +156,7 @@ private async Task CleanupResolversAsyncCore()
private ResolverEntry CreateResolver(string serviceName)
{
- var resolver = _resolverProvider.CreateResolver(serviceName);
+ var resolver = _resolverProvider.CreateWatcher(serviceName);
resolver.Start();
return new ResolverEntry(resolver);
}
@@ -182,7 +181,7 @@ public bool CanExpire()
return (status & (CountMask | RecentUseFlag)) == 0;
}
- public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken)
+ public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken)
{
try
{
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs
deleted file mode 100644
index 415a2192c3..0000000000
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-
-using Microsoft.Extensions.Primitives;
-
-namespace Microsoft.Extensions.ServiceDiscovery.Abstractions;
-
-///
-/// Options for service endpoint resolvers.
-///
-public sealed class ServiceEndPointResolverOptions
-{
- ///
- /// Gets or sets the period between polling resolvers which are in a pending state and do not support refresh notifications via .
- ///
- public TimeSpan PendingStatusRefreshPeriod { get; set; } = TimeSpan.FromSeconds(15);
-
- ///
- /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via .
- ///
- public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60);
-}
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs
index 1786481106..78a8f84b55 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery;
@@ -19,11 +18,11 @@ private sealed partial class Log
[LoggerMessage(3, LogLevel.Debug, "Resolved {Count} endpoints for service '{ServiceName}': {EndPoints}.", EventName = "ResolutionSucceeded")]
public static partial void ResolutionSucceededCore(ILogger logger, int count, string serviceName, string endPoints);
- public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointCollection endPoints)
+ public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndPointSource endPointSource)
{
if (logger.IsEnabled(LogLevel.Debug))
{
- ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString)));
+ ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString)));
}
static string GetEndPointString(ServiceEndPoint ep)
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs
index 8936d3722b..9b1069d31e 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs
@@ -4,34 +4,32 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ExceptionServices;
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.Primitives;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
namespace Microsoft.Extensions.ServiceDiscovery;
///
/// Watches for updates to the collection of resolved endpoints for a specified service.
///
-public sealed partial class ServiceEndPointWatcher(
+internal sealed partial class ServiceEndPointWatcher(
IServiceEndPointProvider[] resolvers,
ILogger logger,
string serviceName,
TimeProvider timeProvider,
- IOptions options) : IAsyncDisposable
+ IOptions options) : IAsyncDisposable
{
private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true);
private readonly object _lock = new();
private readonly ILogger _logger = logger;
private readonly TimeProvider _timeProvider = timeProvider;
- private readonly ServiceEndPointResolverOptions _options = options.Value;
+ private readonly ServiceDiscoveryOptions _options = options.Value;
private readonly IServiceEndPointProvider[] _resolvers = resolvers;
private readonly CancellationTokenSource _disposalCancellation = new();
private ITimer? _pollingTimer;
- private ServiceEndPointCollection? _cachedEndPoints;
+ private ServiceEndPointSource? _cachedEndPoints;
private Task _refreshTask = Task.CompletedTask;
private volatile CacheStatus _cacheState;
@@ -59,23 +57,23 @@ public void Start()
///
/// A .
/// A collection of resolved endpoints for the service.
- public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default)
+ public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default)
{
ThrowIfNoResolvers();
// If the cache is valid, return the cached value.
if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached)
{
- return new ValueTask(cached);
+ return new ValueTask(cached);
}
// Otherwise, ensure the cache is being refreshed
// Wait for the cache refresh to complete and return the cached value.
return GetEndPointsInternal(cancellationToken);
- async ValueTask GetEndPointsInternal(CancellationToken cancellationToken)
+ async ValueTask GetEndPointsInternal(CancellationToken cancellationToken)
{
- ServiceEndPointCollection? result;
+ ServiceEndPointSource? result;
do
{
await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false);
@@ -126,79 +124,48 @@ private async Task RefreshAsyncInternal()
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
var cancellationToken = _disposalCancellation.Token;
Exception? error = null;
- ServiceEndPointCollection? newEndPoints = null;
+ ServiceEndPointSource? newEndPoints = null;
CacheStatus newCacheState;
- ResolutionStatus status = ResolutionStatus.Success;
- while (true)
+ try
{
- try
+ Log.ResolvingEndPoints(_logger, ServiceName);
+ var builder = new ServiceEndPointBuilder();
+ foreach (var resolver in _resolvers)
{
- var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection());
- status = ResolutionStatus.Success;
- Log.ResolvingEndPoints(_logger, ServiceName);
- foreach (var resolver in _resolvers)
- {
- var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false);
- status = CombineStatus(status, resolverStatus);
- }
+ await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false);
+ }
- var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection);
- var statusCode = status.StatusCode;
- if (statusCode != ResolutionStatusCode.Success)
+ var endPoints = builder.Build();
+ newCacheState = CacheStatus.Valid;
+
+ lock (_lock)
+ {
+ // Check if we need to poll for updates or if we can register for change notification callbacks.
+ if (endPoints.ChangeToken.ActiveChangeCallbacks)
{
- if (statusCode is ResolutionStatusCode.Pending)
- {
- // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again.
- Log.ResolutionPending(_logger, ServiceName);
- await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false);
- continue;
- }
- else if (statusCode is ResolutionStatusCode.Cancelled)
+ // Initiate a background refresh, if necessary.
+ endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this);
+ if (_pollingTimer is { } timer)
{
- newCacheState = CacheStatus.Invalid;
- error = status.Exception ?? new OperationCanceledException();
- break;
- }
- else if (statusCode is ResolutionStatusCode.Error)
- {
- newCacheState = CacheStatus.Invalid;
- error = status.Exception;
- break;
+ _pollingTimer = null;
+ timer.Dispose();
}
}
-
- lock (_lock)
+ else
{
- // Check if we need to poll for updates or if we can register for change notification callbacks.
- if (endPoints.ChangeToken.ActiveChangeCallbacks)
- {
- // Initiate a background refresh, if necessary.
- endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this);
- if (_pollingTimer is { } timer)
- {
- _pollingTimer = null;
- timer.Dispose();
- }
- }
- else
- {
- SchedulePollingTimer();
- }
-
- // The cache is valid
- newEndPoints = endPoints;
- newCacheState = CacheStatus.Valid;
- break;
+ SchedulePollingTimer();
}
+
+ // The cache is valid
+ newEndPoints = endPoints;
+ newCacheState = CacheStatus.Valid;
}
- catch (Exception exception)
- {
- error = exception;
- newCacheState = CacheStatus.Invalid;
- SchedulePollingTimer();
- status = CombineStatus(status, ResolutionStatus.FromException(exception));
- break;
- }
+ }
+ catch (Exception exception)
+ {
+ error = exception;
+ newCacheState = CacheStatus.Invalid;
+ SchedulePollingTimer();
}
// If there was an error, the cache must be invalid.
@@ -215,7 +182,7 @@ private async Task RefreshAsyncInternal()
if (OnEndPointsUpdated is { } callback)
{
- callback(new(newEndPoints, status));
+ callback(new(newEndPoints, error));
}
lock (_lock)
@@ -255,48 +222,6 @@ private void SchedulePollingTimer()
}
}
- private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus)
- {
- if (existing.StatusCode > newStatus.StatusCode)
- {
- return existing;
- }
-
- var code = (ResolutionStatusCode)Math.Max((int)existing.StatusCode, (int)newStatus.StatusCode);
- Exception? exception;
- if (existing.Exception is not null && newStatus.Exception is not null)
- {
- List exceptions = new();
- AddExceptions(existing.Exception, exceptions);
- AddExceptions(newStatus.Exception, exceptions);
- exception = new AggregateException(exceptions);
- }
- else
- {
- exception = existing.Exception ?? newStatus.Exception;
- }
-
- var message = code switch
- {
- ResolutionStatusCode.Error => exception!.Message ?? "Error",
- _ => code.ToString(),
- };
-
- return new ResolutionStatus(code, exception, message);
-
- static void AddExceptions(Exception? exception, List exceptions)
- {
- if (exception is AggregateException ae)
- {
- exceptions.AddRange(ae.InnerExceptions);
- }
- else if (exception is not null)
- {
- exceptions.Add(exception);
- }
- }
- }
-
///
public async ValueTask DisposeAsync()
{
@@ -328,48 +253,6 @@ private enum CacheStatus
Valid
}
- private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken)
- {
- if (changeToken.HasChanged)
- {
- return;
- }
-
- TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously);
- IDisposable? changeTokenRegistration = null;
- IDisposable? cancellationRegistration = null;
- IDisposable? pollPeriodRegistration = null;
- CancellationTokenSource? timerCancellation = null;
-
- try
- {
- // Either wait for a callback or poll externally.
- if (changeToken.ActiveChangeCallbacks)
- {
- changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion);
- }
- else
- {
- timerCancellation = new(pollPeriod);
- pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion);
- }
-
- if (cancellationToken.CanBeCanceled)
- {
- cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion);
- }
-
- await completion.Task.ConfigureAwait(false);
- }
- finally
- {
- changeTokenRegistration?.Dispose();
- cancellationRegistration?.Dispose();
- pollPeriodRegistration?.Dispose();
- timerCancellation?.Dispose();
- }
- }
-
private void ThrowIfNoResolvers()
{
if (_resolvers.Length == 0)
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs
similarity index 90%
rename from src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs
index d7835f26d0..69f565eb8e 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs
@@ -2,11 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
namespace Microsoft.Extensions.ServiceDiscovery;
-partial class ServiceEndPointResolverFactory
+partial class ServiceEndPointWatcherFactory
{
private sealed partial class Log
{
diff --git a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs
similarity index 69%
rename from src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs
index c545c82e9e..90f62ab059 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs
@@ -3,38 +3,42 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
using Microsoft.Extensions.ServiceDiscovery.PassThrough;
namespace Microsoft.Extensions.ServiceDiscovery;
///
-/// Creates service endpoint resolvers.
+/// Creates service endpoint watchers.
///
-public partial class ServiceEndPointResolverFactory(
- IEnumerable resolvers,
+internal sealed partial class ServiceEndPointWatcherFactory(
+ IEnumerable resolvers,
ILogger resolverLogger,
- IOptions options,
+ IOptions options,
TimeProvider timeProvider)
{
- private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers
+ private readonly IServiceEndPointProviderFactory[] _resolverProviders = resolvers
.Where(r => r is not PassThroughServiceEndPointResolverProvider)
.Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray();
private readonly ILogger _logger = resolverLogger;
private readonly TimeProvider _timeProvider = timeProvider;
- private readonly IOptions _options = options;
+ private readonly IOptions _options = options;
///
/// Creates a service endpoint resolver for the provided service name.
///
- public ServiceEndPointWatcher CreateResolver(string serviceName)
+ public ServiceEndPointWatcher CreateWatcher(string serviceName)
{
ArgumentNullException.ThrowIfNull(serviceName);
+ if (!ServiceEndPointQuery.TryParse(serviceName, out var query))
+ {
+ throw new ArgumentException("The provided input was not in a valid format. It must be a valid URI.", nameof(serviceName));
+ }
+
List? resolvers = null;
foreach (var factory in _resolverProviders)
{
- if (factory.TryCreateResolver(serviceName, out var resolver))
+ if (factory.TryCreateProvider(query, out var resolver))
{
resolvers ??= [];
resolvers.Add(resolver);
diff --git a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs
similarity index 86%
rename from src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs
rename to src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs
index 6d3132da88..6b5b07d199 100644
--- a/src/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs
+++ b/src/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Net;
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery;
/// An endpoint represented by a .
///
/// The .
-public sealed class UriEndPoint(Uri uri) : EndPoint
+internal sealed class UriEndPoint(Uri uri) : EndPoint
{
///
/// Gets the associated with this endpoint.
diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs
index 7e2ef478ef..25cd88a143 100644
--- a/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs
+++ b/tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs
@@ -8,14 +8,14 @@
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
using Xunit;
namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests;
///
/// Tests for and .
-/// These also cover and by extension.
+/// These also cover and by extension.
///
public class DnsSrvServiceEndPointResolverTests
{
@@ -103,9 +103,9 @@ public async Task ResolveServiceEndPoint_Dns()
.AddServiceDiscoveryCore()
.AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns")
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -114,14 +114,13 @@ public async Task ResolveServiceEndPoint_Dns()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Equal(3, initialResult.EndPoints.Count);
- var eps = initialResult.EndPoints;
+ Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count);
+ var eps = initialResult.EndPointSource.EndPoints;
Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint);
Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint);
Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.Null(hostNameFeature);
@@ -190,9 +189,9 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo
.AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns");
};
var services = serviceCollection.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -200,20 +199,19 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo
resolver.Start();
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
- Assert.Null(initialResult.Status.Exception);
+ Assert.Null(initialResult.Exception);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
if (dnsFirst)
{
// We expect only the results from the DNS provider.
- Assert.Equal(3, initialResult.EndPoints.Count);
- var eps = initialResult.EndPoints;
+ Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count);
+ var eps = initialResult.EndPointSource.EndPoints;
Assert.Equal(new IPEndPoint(IPAddress.Parse("10.10.10.10"), 8888), eps[0].EndPoint);
Assert.Equal(new IPEndPoint(IPAddress.IPv6Loopback, 9999), eps[1].EndPoint);
Assert.Equal(new DnsEndPoint("remotehost", 7777), eps[2].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.NotNull(hostNameFeature);
@@ -223,11 +221,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo
else
{
// We expect only the results from the Configuration provider.
- Assert.Equal(2, initialResult.EndPoints.Count);
- Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint);
- Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint);
+ Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count);
+ Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPointSource.EndPoints[0].EndPoint);
+ Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPointSource.EndPoints[1].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.Null(hostNameFeature);
@@ -271,9 +269,9 @@ public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken()
.AddServiceDiscovery()
.AddConfigurationServiceEndPointResolver()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointResolver resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var channel = Channel.CreateUnbounded();
diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs
index f35ffa2026..6d8091f026 100644
--- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs
+++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs
@@ -5,15 +5,15 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery.Configuration;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
using Xunit;
namespace Microsoft.Extensions.ServiceDiscovery.Tests;
///
-/// Tests for and .
-/// These also cover and by extension.
+/// Tests for .
+/// These also cover and by extension.
///
public class ConfigurationServiceEndPointResolverTests
{
@@ -29,9 +29,9 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult()
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndPointResolver()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -40,11 +40,10 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- var ep = Assert.Single(initialResult.EndPoints);
+ var ep = Assert.Single(initialResult.EndPointSource.EndPoints);
Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.Null(hostNameFeature);
@@ -67,12 +66,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme()
.AddConfigurationServiceEndPointResolver()
.Configure(o => o.AllowedSchemes = ["https"])
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
// Explicitly specifying http.
// We should get no endpoint back because http is not allowed by configuration.
- await using ((resolver = resolverFactory.CreateResolver("http://_foo.basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -81,13 +80,12 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Empty(initialResult.EndPoints);
+ Assert.Empty(initialResult.EndPointSource.EndPoints);
}
// Specifying either https or http.
// The result should be that we only get the http endpoint back.
- await using ((resolver = resolverFactory.CreateResolver("https+http://_foo.basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -96,14 +94,13 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- var ep = Assert.Single(initialResult.EndPoints);
+ var ep = Assert.Single(initialResult.EndPointSource.EndPoints);
Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint);
}
// Specifying either https or http, but in reverse.
// The result should be that we only get the http endpoint back.
- await using ((resolver = resolverFactory.CreateResolver("http+https://_foo.basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -112,8 +109,7 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- var ep = Assert.Single(initialResult.EndPoints);
+ var ep = Assert.Single(initialResult.EndPointSource.EndPoints);
Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint);
}
}
@@ -135,9 +131,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults()
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true)
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -146,12 +142,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Equal(2, initialResult.EndPoints.Count);
- Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint);
- Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint);
+ Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count);
+ Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint);
+ Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.NotNull(hostNameFeature);
@@ -160,7 +155,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults()
}
// Request either https or http. Since there are only http endpoints, we should get only http endpoints back.
- await using ((resolver = resolverFactory.CreateResolver("https+http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -169,12 +164,11 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Equal(2, initialResult.EndPoints.Count);
- Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint);
- Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPoints[1].EndPoint);
+ Assert.Equal(2, initialResult.EndPointSource.EndPoints.Count);
+ Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint);
+ Assert.Equal(new UriEndPoint(new Uri("http://remotehost:9090")), initialResult.EndPointSource.EndPoints[1].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.NotNull(hostNameFeature);
@@ -204,9 +198,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols()
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndPointResolver()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -215,13 +209,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Equal(3, initialResult.EndPoints.Count);
- Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint);
- Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint);
- Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPoints[2].EndPoint);
+ Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count);
+ Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint);
+ Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint);
+ Assert.Equal(new UriEndPoint(new Uri("http://remotehost:4444")), initialResult.EndPointSource.EndPoints[2].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.Null(hostNameFeature);
@@ -250,9 +243,9 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe
.AddServiceDiscoveryCore()
.AddConfigurationServiceEndPointResolver()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -261,17 +254,16 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- Assert.Equal(3, initialResult.EndPoints.Count);
+ Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count);
// These must be treated as HTTPS by the HttpClient middleware, but that is not the responsibility of the resolver.
- Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint);
- Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPoints[1].EndPoint);
+ Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPointSource.EndPoints[0].EndPoint);
+ Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.EndPoints[1].EndPoint);
// We expect the HTTPS endpoint back but not the HTTP one.
- Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPoints[2].EndPoint);
+ Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndPointSource.EndPoints[2].EndPoint);
- Assert.All(initialResult.EndPoints, ep =>
+ Assert.All(initialResult.EndPointSource.EndPoints, ep =>
{
var hostNameFeature = ep.Features.Get();
Assert.Null(hostNameFeature);
diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs
index d8adcbca52..643bbfad44 100644
--- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs
+++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs
@@ -5,8 +5,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Memory;
using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
using Microsoft.Extensions.ServiceDiscovery.PassThrough;
using Xunit;
@@ -14,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests;
///
/// Tests for .
-/// These also cover and by extension.
+/// These also cover and by extension.
///
public class PassThroughServiceEndPointResolverTests
{
@@ -25,9 +24,9 @@ public async Task ResolveServiceEndPoint_PassThrough()
.AddServiceDiscoveryCore()
.AddPassThroughServiceEndPointResolver()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -36,8 +35,7 @@ public async Task ResolveServiceEndPoint_PassThrough()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
- var ep = Assert.Single(initialResult.EndPoints);
+ var ep = Assert.Single(initialResult.EndPointSource.EndPoints);
Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint);
}
}
@@ -57,9 +55,9 @@ public async Task ResolveServiceEndPoint_Superseded()
.AddSingleton(config.Build())
.AddServiceDiscovery() // Adds the configuration and pass-through providers.
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -68,11 +66,10 @@ public async Task ResolveServiceEndPoint_Superseded()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
// We expect the basket service to be resolved from Configuration, not the pass-through provider.
- Assert.Single(initialResult.EndPoints);
- Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint);
+ Assert.Single(initialResult.EndPointSource.EndPoints);
+ Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint);
}
}
@@ -91,9 +88,9 @@ public async Task ResolveServiceEndPoint_Fallback()
.AddSingleton(config.Build())
.AddServiceDiscovery() // Adds the configuration and pass-through providers.
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var tcs = new TaskCompletionSource();
@@ -102,11 +99,10 @@ public async Task ResolveServiceEndPoint_Fallback()
var initialResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(initialResult);
Assert.True(initialResult.ResolvedSuccessfully);
- Assert.Equal(ResolutionStatus.Success, initialResult.Status);
// We expect the CATALOG service to be resolved from the pass-through provider.
- Assert.Single(initialResult.EndPoints);
- Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint);
+ Assert.Single(initialResult.EndPointSource.EndPoints);
+ Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint);
}
}
@@ -128,7 +124,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme()
.BuildServiceProvider();
var resolver = services.GetRequiredService();
- var endPoints = await resolver.GetEndPointsAsync("catalog", default);
- Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint);
+ var result = await resolver.GetEndPointsAsync("catalog", default);
+ Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint);
}
}
diff --git a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs
index 0628bedbe7..f5e506a9b7 100644
--- a/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs
+++ b/tests/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs
@@ -8,14 +8,14 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Primitives;
-using Microsoft.Extensions.ServiceDiscovery.Abstractions;
using Microsoft.Extensions.ServiceDiscovery.Http;
+using Microsoft.Extensions.ServiceDiscovery.Internal;
using Xunit;
namespace Microsoft.Extensions.ServiceDiscovery.Tests;
///
-/// Tests for and .
+/// Tests for and .
///
public class ServiceEndPointResolverTests
{
@@ -25,8 +25,8 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws()
var services = new ServiceCollection()
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
- var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket"));
+ var resolverFactory = services.GetRequiredService();
+ var exception = Assert.Throws(() => resolverFactory.CreateWatcher("https://basket"));
Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message);
}
@@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws()
var services = new ServiceCollection()
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions()));
+ var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions()));
var exception = Assert.Throws(resolverFactory.Start);
Assert.Equal("No service endpoint resolvers are configured.", exception.Message);
exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync());
@@ -49,35 +49,34 @@ public void ResolveServiceEndPoint_NullServiceName_Throws()
var services = new ServiceCollection()
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
- Assert.Throws(() => resolverFactory.CreateResolver(null!));
+ var resolverFactory = services.GetRequiredService();
+ Assert.Throws(() => resolverFactory.CreateWatcher(null!));
}
[Fact]
- public async Task UseServiceDiscovery_NoResolvers_Throws()
+ public async Task AddServiceDiscovery_NoResolvers_Throws()
{
var serviceCollection = new ServiceCollection();
- serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo"))
- .UseServiceDiscovery();
+ serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")).AddServiceDiscovery();
var services = serviceCollection.BuildServiceProvider();
var client = services.GetRequiredService().CreateClient("foo");
var exception = await Assert.ThrowsAsync(async () => await client.GetStringAsync("/"));
Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message);
}
- private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider
+ private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory
{
- public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
+ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver)
{
bool result;
- (result, resolver) = createResolverDelegate(serviceName);
+ (result, resolver) = createResolverDelegate(query);
return result;
}
}
- private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider
+ private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider
{
- public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken);
+ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken);
public ValueTask DisposeAsync() => disposeAsync();
}
@@ -101,18 +100,18 @@ public async Task ResolveServiceEndPoint()
disposeAsync: () => default);
var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver));
var services = new ServiceCollection()
- .AddSingleton(resolverProvider)
+ .AddSingleton(resolverProvider)
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
- var initialEndPoints = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false);
- Assert.NotNull(initialEndPoints);
- var sep = Assert.Single(initialEndPoints);
+ var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false);
+ Assert.NotNull(initialResult);
+ var sep = Assert.Single(initialResult.EndPoints);
var ip = Assert.IsType(sep.EndPoint);
Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address);
Assert.Equal(8080, ip.Port);
@@ -124,10 +123,9 @@ public async Task ResolveServiceEndPoint()
cts[0].Cancel();
var resolverResult = await tcs.Task.ConfigureAwait(false);
Assert.NotNull(resolverResult);
- Assert.Equal(ResolutionStatus.Success, resolverResult.Status);
Assert.True(resolverResult.ResolvedSuccessfully);
- Assert.Equal(2, resolverResult.EndPoints.Count);
- var endpoints = resolverResult.EndPoints.Select(ep => ep.EndPoint).OfType().ToList();
+ Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count);
+ var endpoints = resolverResult.EndPointSource.EndPoints.Select(ep => ep.EndPoint).OfType().ToList();
endpoints.Sort((l, r) => l.Port - r.Port);
Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080), endpoints[0]);
Assert.Equal(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888), endpoints[1]);
@@ -154,15 +152,15 @@ public async Task ResolveServiceEndPointOneShot()
disposeAsync: () => default);
var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver));
var services = new ServiceCollection()
- .AddSingleton(resolverProvider)
+ .AddSingleton(resolverProvider)
.AddServiceDiscoveryCore()
.BuildServiceProvider();
var resolver = services.GetRequiredService();
Assert.NotNull(resolver);
- var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false);
- Assert.NotNull(initialEndPoints);
- var sep = Assert.Single(initialEndPoints);
+ var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false);
+ Assert.NotNull(initialResult);
+ var sep = Assert.Single(initialResult.EndPoints);
var ip = Assert.IsType(sep.EndPoint);
Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address);
Assert.Equal(8080, ip.Port);
@@ -190,12 +188,11 @@ public async Task ResolveHttpServiceEndPointOneShot()
disposeAsync: () => default);
var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver));
var services = new ServiceCollection()
- .AddSingleton(fakeResolverProvider)
+ .AddSingleton(fakeResolverProvider)
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var selectorProvider = services.GetRequiredService();
- var resolverProvider = services.GetRequiredService();
- await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System);
+ var resolverProvider = services.GetRequiredService();
+ await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, TimeProvider.System);
Assert.NotNull(resolver);
var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket");
@@ -232,25 +229,24 @@ public async Task ResolveServiceEndPoint_ThrowOnReload()
collection.AddChangeToken(new CancellationChangeToken(cts[0].Token));
collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080)));
- return ResolutionStatus.Success;
},
disposeAsync: () => default);
var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver));
var services = new ServiceCollection()
- .AddSingleton(resolverProvider)
+ .AddSingleton(resolverProvider)
.AddServiceDiscoveryCore()
.BuildServiceProvider();
- var resolverFactory = services.GetRequiredService();
+ var resolverFactory = services.GetRequiredService();
ServiceEndPointWatcher resolver;
- await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false))
+ await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false))
{
Assert.NotNull(resolver);
var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false);
sem.Release(1);
var initialEndPoints = await initialEndPointsTask;
Assert.NotNull(initialEndPoints);
- Assert.Single(initialEndPoints);
+ Assert.Single(initialEndPoints.EndPoints);
// Tell the resolver to throw on the next resolve call and then trigger a reload.
throwOnNextResolve[0] = true;
@@ -283,9 +279,9 @@ public async Task ResolveServiceEndPoint_ThrowOnReload()
var task = resolver.GetEndPointsAsync(CancellationToken.None);
sem.Release(1);
- var endPoints = await task.ConfigureAwait(false);
- Assert.NotSame(initialEndPoints, endPoints);
- var sep = Assert.Single(endPoints);
+ var result = await task.ConfigureAwait(false);
+ Assert.NotSame(initialEndPoints, result);
+ var sep = Assert.Single(result.EndPoints);
var ip = Assert.IsType(sep.EndPoint);
Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address);
Assert.Equal(8080, ip.Port);
diff --git a/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs b/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs
index ecbc1fd83b..9fa1a0e1bb 100644
--- a/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs
+++ b/tests/TestingAppHost1/TestingAppHost1.ServiceDefaults/Extensions.cs
@@ -25,7 +25,7 @@ public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBu
http.AddStandardResilienceHandler();
// Turn on service discovery by default
- http.UseServiceDiscovery();
+ http.AddServiceDiscovery();
});
// Uncomment the following to restrict the allowed schemes for service discovery.
diff --git a/tests/send-to-helix.proj b/tests/send-to-helix.proj
index 01f89a3b86..cc328aef3c 100644
--- a/tests/send-to-helix.proj
+++ b/tests/send-to-helix.proj
@@ -34,6 +34,14 @@
<_TestRunCommandArguments Include="-s .runsettings" />
<_TestRunCommandArguments Include="$(_TestNameEnvVar).dll" />
<_TestRunCommandArguments Include="--ResultsDirectory:$(_HelixLogsPath)" />
+ <_TestRunCommandArguments Include="--blame-hang" />
+ <_TestRunCommandArguments Include="--blame-hang-dump-type" />
+ <_TestRunCommandArguments Include="full" />
+ <_TestRunCommandArguments Include="--blame-hang-timeout" />
+ <_TestRunCommandArguments Include="10m" />
+ <_TestRunCommandArguments Include="--blame-crash" />
+ <_TestRunCommandArguments Include="--blame-crash-dump-type" />
+ <_TestRunCommandArguments Include="full" />
diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs
index 9efd9b56e0..d56bf5a8d2 100644
--- a/tests/testproject/TestProject.AppHost/TestProgram.cs
+++ b/tests/testproject/TestProject.AppHost/TestProgram.cs
@@ -158,7 +158,7 @@ public DistributedApplication Build()
public void Dispose() => App?.Dispose();
///
- /// Writes the allocatedEndpoint endpoints to the console in JSON format.
+ /// Writes the allocated endpoints to the console in JSON format.
/// This allows for easier consumption by the external test process.
///
private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook