From f65b56f669e98ec065e0a31cb1bb977c40134a87 Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Thu, 28 Sep 2023 15:17:57 -0700 Subject: [PATCH 01/77] Initial commit --- .../Features/IEndPointHealthFeature.cs | 12 + .../Features/IEndPointLoadFeature.cs | 11 + .../IServiceEndPointResolver.cs | 18 + .../IServiceEndPointResolverProvider.cs | 20 + .../IServiceEndPointSelector.cs | 23 ++ .../IServiceEndPointSelectorProvider.cs | 16 + .../Internal/ServiceEndPointImpl.cs | 24 ++ ...sions.ServiceDiscovery.Abstractions.csproj | 18 + .../ResolutionStatus.cs | 100 +++++ .../ResolutionStatusCode.cs | 40 ++ .../ServiceEndPoint.cs | 44 +++ .../ServiceEndPointCollection.cs | 87 +++++ .../ServiceEndPointCollectionSource.cs | 57 +++ .../ServiceEndPointResolverResult.cs | 30 ++ .../DnsServiceEndPointResolver.Log.cs | 40 ++ .../DnsServiceEndPointResolver.cs | 282 ++++++++++++++ .../DnsServiceEndPointResolverOptions.cs | 43 +++ .../DnsServiceEndPointResolverProvider.cs | 149 ++++++++ .../HostingExtensions.cs | 45 +++ ...oft.Extensions.ServiceDiscovery.Dns.csproj | 22 ++ ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 19 + ...ReverseProxyServiceCollectionExtensions.cs | 43 +++ .../ServiceDiscoveryDestinationResolver.cs | 93 +++++ ...viceDiscoveryForwarderHttpClientFactory.cs | 20 + .../ConfigurationServiceEndPointResolver.cs | 146 +++++++ ...igurationServiceEndPointResolverOptions.cs | 15 + ...gurationServiceEndPointResolverProvider.cs | 28 ++ .../HostingExtensions.cs | 83 ++++ .../Http/HttpClientBuilderExtensions.cs | 110 ++++++ .../Http/HttpServiceEndPointResolver.cs | 258 +++++++++++++ .../Http/ResolvingHttpClientHandler.cs | 45 +++ .../Http/ResolvingHttpDelegatingHandler.cs | 99 +++++ .../PassThroughServiceEndPointResolver.cs | 45 +++ .../Internal/ServiceNameParts.cs | 97 +++++ .../PickFirstServiceEndPointSelector.cs | 29 ++ ...ickFirstServiceEndPointSelectorProvider.cs | 18 + ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 +++ ...oChoicesServiceEndPointSelectorProvider.cs | 18 + .../RandomServiceEndPointSelector.cs | 29 ++ .../RandomServiceEndPointSelectorProvider.cs | 18 + .../RoundRobinServiceEndPointSelector.cs | 30 ++ ...undRobinServiceEndPointSelectorProvider.cs | 18 + ...crosoft.Extensions.ServiceDiscovery.csproj | 16 + .../ServiceEndPointResolver.cs | 358 ++++++++++++++++++ .../ServiceEndPointResolverFactory.cs | 56 +++ .../ServiceEndPointResolverOptions.cs | 22 ++ .../ServiceEndPointResolverRegistry.cs | 240 ++++++++++++ .../DnsServiceEndPointResolverTests.cs | 285 ++++++++++++++ ...tensions.ServiceDiscovery.Dns.Tests.csproj | 20 + ...nfigurationServiceEndPointResolverTests.cs | 135 +++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 19 + ...PassThroughServiceEndPointResolverTests.cs | 112 ++++++ .../ServiceEndPointResolverTests.cs | 192 ++++++++++ 53 files changed, 3847 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs new file mode 100644 index 00000000000..50276e05ecb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -0,0 +1,12 @@ +// 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; + +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. + void ReportHealth(TimeSpan responseTime, Exception? exception); +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs new file mode 100644 index 00000000000..d58f23c7775 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -0,0 +1,11 @@ +// 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; + +public interface IEndPointLoadFeature +{ + // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + public double CurrentLoad { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs new file mode 100644 index 00000000000..c228847c568 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -0,0 +1,18 @@ +// 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 resolving endpoints for a service. +/// +public interface IServiceEndPointResolver : IAsyncDisposable +{ + /// + /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. + /// + /// 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); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..9ad9e3ae7b8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Creates instances. +/// +public interface IServiceEndPointResolverProvider +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the resolver for. + /// The resolver. + /// if the resolver was created, otherwise. + bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs new file mode 100644 index 00000000000..e2ffbd0421f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs @@ -0,0 +1,23 @@ +// 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 from a collection of endpoints. +/// +public interface IServiceEndPointSelector +{ + /// + /// Sets the collection of endpoints which this instance will select from. + /// + /// The collection of endpoints to select from. + void SetEndPoints(ServiceEndPointCollection endPoints); + + /// + /// Selects an endpoints from the collection provided by the most recent call to . + /// + /// The context. + /// An endpoint. + ServiceEndPoint GetEndPoint(object? context); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..27f4ec4324a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs @@ -0,0 +1,16 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs new file mode 100644 index 00000000000..0b54f5a19d0 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -0,0 +1,24 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +internal sealed class ServiceEndPointImpl : 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 string? ToString() => _endPoint.ToString(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj new file mode 100644 index 00000000000..f89f1ba02ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -0,0 +1,18 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs new file mode 100644 index 00000000000..152571bbb74 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs @@ -0,0 +1,100 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs new file mode 100644 index 00000000000..7157eac758f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs @@ -0,0 +1,40 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs new file mode 100644 index 00000000000..9dc4675dade --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs @@ -0,0 +1,44 @@ +// 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.AspNetCore.Http.Features; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Represents an endpoint for a service. +/// +[DebuggerDisplay("{GetEndPointString(),nq}")] +public abstract class ServiceEndPoint +{ + /// + /// Gets the endpoint. + /// + public abstract EndPoint EndPoint { get; } + + /// + /// Gets the collection of endpoint features. + /// + public abstract IFeatureCollection Features { get; } + + /// + /// Creates a new . + /// + /// The endpoint being represented. + /// Features of the endpoint. + /// A newly initialized . + public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); + + /// + /// Gets a string representation of the . + /// + /// A string representation of the . + public virtual string GetEndPointString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + IPEndPoint ip => ip.ToString(), + _ => EndPoint.ToString()! + }; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs new file mode 100644 index 00000000000..57919f949c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs @@ -0,0 +1,87 @@ +// 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; + +/// +/// Represents an immutable collection of service endpoints. +/// +[DebuggerDisplay("{ToString(),nq}")] +[DebuggerTypeProxy(nameof(ServiceEndPointCollectionDebuggerView))] +public class ServiceEndPointCollection : IReadOnlyList +{ + private readonly List? _endpoints; + + /// + /// 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) + { + 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. + /// + public string ServiceName { get; } + + /// + /// Gets the change token which indicates when this collection should be refreshed. + /// + public IChangeToken ChangeToken { get; } + + /// + /// Gets the feature collection. + /// + 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() + { + if (_endpoints is not { } eps) + { + return "[]"; + } + + return $"[{string.Join(", ", eps)}]"; + } + + private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointCollection value) + { + public string ServiceName => value.ServiceName; + + public IChangeToken ChangeToken => value.ChangeToken; + + public IFeatureCollection Features => value.Features; + + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public ServiceEndPoint[] EndPoints => value.ToArray(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs new file mode 100644 index 00000000000..94f274a38e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs @@ -0,0 +1,57 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..9179ed2f113 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/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.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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..6ace4ac983e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs @@ -0,0 +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 Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal partial class DnsServiceEndPointResolver +{ + private static partial class Log + { + [LoggerMessage(1, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using DNS SRV lookup for name '{RecordName}'.", EventName = "SrvQuery")] + public static partial void SrvQuery(ILogger logger, string serviceName, string recordName); + + [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] + public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); + + public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) + { + if (logger.IsEnabled(LogLevel.Trace)) + { + DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); + } + else if (logger.IsEnabled(LogLevel.Debug)) + { + DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); + } + } + + [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] + public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); + + [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] + public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); + + [LoggerMessage(4, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs new file mode 100644 index 00000000000..63d60d5318d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -0,0 +1,282 @@ +// 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 DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// A service end point resolver that uses DNS to resolve the service end points. +/// +internal sealed partial class DnsServiceEndPointResolver : IServiceEndPointResolver +{ + private readonly object _lock = new(); + private readonly string _serviceName; + private readonly Stopwatch _lastRefreshTimer = new(); + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly CancellationTokenSource _disposeCancellation = new(); + private readonly IDnsQuery _dnsClient; + private readonly TimeProvider _timeProvider; + private Task _resolveTask = Task.CompletedTask; + private ResolutionStatus _lastStatus; + private IChangeToken? _lastChangeToken; + private CancellationTokenSource _lastCollectionCancellation; + private List? _lastEndPointCollection; + private readonly string _addressRecordName; + private readonly string _srvRecordName; + private readonly int _defaultPort; + private TimeSpan _nextRefreshPeriod; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The name used to resolve the address of this service. + /// The name used to resolve this service's SRV record in DNS. + /// The default port to use for endpoints. + /// The options. + /// The logger. + /// The DNS client. + /// The time provider. + public DnsServiceEndPointResolver( + string serviceName, + string addressRecordName, + string srvRecordName, + int defaultPort, + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) + { + _serviceName = serviceName; + _options = options; + _logger = logger; + _lastEndPointCollection = null; + _addressRecordName = addressRecordName; + _srvRecordName = srvRecordName; + _defaultPort = defaultPort; + _dnsClient = dnsClient; + _nextRefreshPeriod = _options.CurrentValue.MinRetryPeriod; + _timeProvider = timeProvider; + var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(_options.CurrentValue.DefaultRefreshPeriod); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + } + + /// + public async ValueTask ResolveAsync(ServiceEndPointCollectionSource 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) + { + return ResolutionStatus.None; + } + + if (ShouldRefresh()) + { + Task resolveTask; + lock (_lock) + { + if (_resolveTask.IsCompleted && ShouldRefresh()) + { + _resolveTask = ResolveAsyncInternal(); + } + + resolveTask = _resolveTask; + } + + await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + lock (_lock) + { + if (_lastEndPointCollection is { Count: > 0 } eps) + { + foreach (var ep in eps) + { + endPoints.EndPoints.Add(ep); + } + } + + if (_lastChangeToken is not null) + { + endPoints.AddChangeToken(_lastChangeToken); + } + + return _lastStatus; + } + } + + private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || _lastRefreshTimer.Elapsed >= _nextRefreshPeriod; + + private async Task ResolveAsyncInternal() + { + var endPoints = new List(); + var options = _options.CurrentValue; + var ttl = options.DefaultRefreshPeriod; + try + { + if (options.UseSrvQuery) + { + Log.SrvQuery(_logger, _serviceName, _srvRecordName); + var result = await _dnsClient.QueryAsync(_srvRecordName, QueryType.SRV).ConfigureAwait(false); + if (result.HasError) + { + SetException(CreateException(result.ErrorMessage), ttl); + return; + } + + var lookupMapping = new Dictionary(); + foreach (var record in result.Additionals) + { + ttl = MinTtl(record, ttl); + lookupMapping[record.DomainName] = record; + } + + var srvRecords = result.Answers.OfType(); + foreach (var record in srvRecords) + { + if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) + { + continue; + } + + ttl = MinTtl(record, ttl); + if (targetRecord is AddressRecord addressRecord) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + } + else if (targetRecord is CNameRecord canonicalNameRecord) + { + endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + } + } + } + else + { + Log.AddressQuery(_logger, _serviceName, _addressRecordName); + var addresses = await System.Net.Dns.GetHostAddressesAsync(_addressRecordName, _disposeCancellation.Token).ConfigureAwait(false); + foreach (var address in addresses) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, _defaultPort))); + } + + if (endPoints.Count == 0) + { + SetException(CreateException(), ttl); + return; + } + } + + SetResult(endPoints, ttl); + } + catch (Exception exception) + { + SetException(exception, ttl); + throw; + } + + static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + { + var candidate = TimeSpan.FromSeconds(record.TimeToLive); + return candidate < existing ? candidate : existing; + } + + InvalidOperationException CreateException(string? errorMessage = null) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS records were found for service {_serviceName}: {errorMessage}.", + _ => $"No DNS records were found for service {_serviceName}." + }; + var exception = new InvalidOperationException(msg); + return exception; + } + } + + private void SetException(Exception exception, TimeSpan validityPeriod) => SetResult(endPoints: null, exception, validityPeriod); + private void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); + private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) + { + lock (_lock) + { + var options = _options.CurrentValue; + if (exception is not null) + { + if (_lastStatus.Exception is null) + { + _nextRefreshPeriod = options.MinRetryPeriod; + } + else + { + var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * options.RetryBackOffFactor)); + _nextRefreshPeriod = nextPeriod > options.MaxRetryPeriod ? options.MaxRetryPeriod : nextPeriod; + } + + 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); + } + } + else + { + _lastRefreshTimer.Restart(); + _nextRefreshPeriod = options.DefaultRefreshPeriod; + _lastStatus = ResolutionStatus.Success; + } + + validityPeriod = validityPeriod > TimeSpan.Zero && validityPeriod < _nextRefreshPeriod ? validityPeriod : _nextRefreshPeriod; + _lastCollectionCancellation.Cancel(); + var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(validityPeriod); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + _lastEndPointCollection = endPoints; + } + + if (exception is null) + { + Debug.Assert(endPoints is not null); + Log.DiscoveredEndPoints(_logger, endPoints, _serviceName, validityPeriod); + } + else + { + Log.ResolutionFailed(_logger, exception, _serviceName); + } + } + + /// + public async ValueTask DisposeAsync() + { + _disposeCancellation.Cancel(); + + if (_resolveTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private CancellationTokenSource CreateCancellationTokenSource(TimeSpan validityPeriod) + { + if (validityPeriod <= TimeSpan.Zero) + { + // Do not invalidate on a timer, but invalidate on refresh. + return new CancellationTokenSource(); + } + else + { + return new CancellationTokenSource(validityPeriod, _timeProvider); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..45adebfea0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -0,0 +1,43 @@ +// 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.Dns; + +/// +/// Options for configuring . +/// +public class DnsServiceEndPointResolverOptions +{ + /// + /// Gets or sets the default refresh period for endpoints resolved from DNS. + /// + public TimeSpan DefaultRefreshPeriod { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the initial period between retries. + /// + public TimeSpan MinRetryPeriod { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the maximum period between retries. + /// + public TimeSpan MaxRetryPeriod { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the retry period growth factor. + /// + public double RetryBackOffFactor { get; set; } = 2; + + /// + /// Gets or sets the default DNS namespace for services resolved via this provider. + /// + /// + /// If not specified, the provider will attempt to infer the namespace. + /// + public string? DnsNamespace { get; set; } + + /// + /// Gets or sets a value indicating whether to use DNS SRV queries to discover host addresses and ports. + /// + public bool UseSrvQuery { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..4a554d7c9e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -0,0 +1,149 @@ +// 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 System.Text.RegularExpressions; +using DnsClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides instances which resolve endpoints from DNS. +/// +internal sealed partial class DnsServiceEndPointResolverProvider : IServiceEndPointResolverProvider +{ + private readonly IOptionsMonitor _options; + private readonly ILogger _logger; + private readonly IDnsQuery _dnsClient; + private readonly TimeProvider _timeProvider; + private readonly string? _defaultNamespace; + + /// + /// Initializes a new instance. + /// + /// The options. + /// The logger. + /// The DNS client. + /// The time provider. + public DnsServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) + { + _options = options; + _logger = logger; + _dnsClient = dnsClient; + _timeProvider = timeProvider; + _defaultNamespace = options.CurrentValue.DnsNamespace ?? GetHostNamespace(); + } + + // RFC 2181 + // DNS hostnames can consist only of letters, digits, dots, and hyphens. + // They must begin with a letter. + // They must end with a letter or a digit. + // Individual segments (between dots) can be no longer than 63 characters. + [GeneratedRegex("^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$")] + private static partial Regex ValidDnsName(); + + // Adapted version of Tim Berners Lee's regex from the URI spec: https://stackoverflow.com/a/26766402 + // Adapted to parse the port into a group and discard groups which we do not care about. + [GeneratedRegex("^(?:([^:/?#]+)://)?([^/?#:]*)?(?::([\\d]+))?")] + private static partial Regex UriRegex(); + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md + // SRV records are available for headless services with named ports. + // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.svc.{zone}" + // We can fetch the namespace from /var/run/secrets/kubernetes.io/serviceaccount/namespace + // 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 dnsServiceName = serviceName; + var dnsNamespace = _defaultNamespace; + var portName = "default"; + var defaultPortNumber = 0; + + // Allow the service name to be expressed as either a URI or a plain DNS name. + var uri = UriRegex().Match(serviceName); + if (uri.Success) + { + if (uri.Groups[1].ValueSpan is { Length: > 0 } uriPortNameSpan) + { + // Override the port name if it was specified in the service name + portName = uriPortNameSpan.ToString(); + } + + if (int.TryParse(uri.Groups[3].ValueSpan, out var uriDefaultPort)) + { + // Override the default port if it was specified in the service name + defaultPortNumber = uriDefaultPort; + } + + // Since the service name was URI-formatted, we should extract the hostname part for resolution. + dnsServiceName = uri.Groups[2].Value; + } + else if (!ValidDnsName().IsMatch(serviceName)) + { + resolver = default; + return false; + } + + // If the DNS name is not qualified, and we have a qualifier, apply it. + if (!dnsServiceName.Contains('.') && dnsNamespace is not null) + { + dnsServiceName = $"{dnsServiceName}.{dnsNamespace}"; + } + + var srvRecordName = $"_{portName}._tcp.{dnsServiceName}"; + resolver = new DnsServiceEndPointResolver(serviceName, dnsServiceName, srvRecordName, defaultPortNumber, _options, _logger, _dnsClient, _timeProvider); + return true; + } + + private static string? GetHostNamespace() => ReadNamespaceFromKubernetesServiceAccount() ?? ReadQualifiedNamespaceFromResolvConf(); + + private static string? ReadNamespaceFromKubernetesServiceAccount() + { + if (OperatingSystem.IsLinux()) + { + // Read the namespace from the Kubernetes pod's service account. + var serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); + if (File.Exists(serviceAccountNamespacePath)) + { + return File.ReadAllText(serviceAccountNamespacePath).Trim(); + } + } + + return null; + } + + private static string? ReadQualifiedNamespaceFromResolvConf() + { + if (OperatingSystem.IsLinux()) + { + var resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); + if (File.Exists(resolveConfPath)) + { + var lines = File.ReadAllLines(resolveConfPath); + foreach (var line in lines) + { + if (line.StartsWith("search ")) + { + var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + + if (components.Length > 1) + { + return components[1]; + } + } + } + } + } + + return default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs new file mode 100644 index 00000000000..49351af386d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DnsClient; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Dns; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extensions for to add service discovery. +/// +public static class HostingExtensions +{ + /// + /// Adds DNS-based service discovery to the . + /// + /// The service collection. + /// The DNS service discovery configuration options. + /// The provided . + public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.Configure(options => options.UseSrvQuery = true); + return services.AddDnsServiceEndPointResolver(configureOptions); + } + + /// + /// Adds DNS-based service discovery to the . + /// + /// The service collection. + /// The DNS service discovery configuration options. + /// The provided . + public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.AddServiceDiscoveryCore(); + services.TryAddSingleton(); + services.AddSingleton(); + var options = services.AddOptions(); + configureOptions?.Invoke(options); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj new file mode 100644 index 00000000000..7f208a3f84a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -0,0 +1,22 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj new file mode 100644 index 00000000000..eaa6cebf61c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -0,0 +1,19 @@ + + + + $(NetCurrent) + enable + enable + true + + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs new file mode 100644 index 00000000000..e52ff65ee2a --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs @@ -0,0 +1,43 @@ +// 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; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions for used to register the ReverseProxy's components. +/// +public static class ReverseProxyServiceCollectionExtensions +{ + /// + /// Provides a implementation which uses service discovery to resolve destinations. + /// + public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) + { + builder.Services.AddServiceDiscoveryCore(); + builder.Services.AddSingleton(); + return builder; + } + + /// + /// Adds the with service discovery support. + /// + public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) + { + return services.AddHttpForwarder().AddServiceDiscoveryForwarderFactory(); + } + + /// + /// Provides a implementation which uses service discovery to resolve service names. + /// + public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs new file mode 100644 index 00000000000..fbcef8c72ad --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -0,0 +1,93 @@ +// 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; +using Yarp.ReverseProxy.Configuration; +using Yarp.ReverseProxy.ServiceDiscovery; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp; + +/// +/// Implementation of which resolves destinations using service discovery. +/// +/// +/// Initializes a new instance. +/// +/// The endpoint resolver registry. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolverRegistry registry) : IDestinationResolver +{ + /// + public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) + { + Dictionary results = new(); + var tasks = new List, IChangeToken ChangeToken)>>(destinations.Count); + foreach (var (destinationId, destinationConfig) in destinations) + { + tasks.Add(ResolveHostAsync(destinationId, destinationConfig, cancellationToken)); + } + + await Task.WhenAll(tasks).ConfigureAwait(false); + var changeTokens = new List(); + foreach (var task in tasks) + { + var (configs, changeToken) = await task.ConfigureAwait(false); + if (changeToken is not null) + { + changeTokens.Add(changeToken); + } + + foreach (var (name, config) in configs) + { + results[name] = config; + } + } + + return new ResolvedDestinationCollection(results, new CompositeChangeToken(changeTokens)); + } + + private async Task<(List<(string Name, DestinationConfig Config)>, IChangeToken ChangeToken)> ResolveHostAsync( + string originalName, + DestinationConfig originalConfig, + CancellationToken cancellationToken) + { + var originalUri = new Uri(originalConfig.Address); + var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; + var serviceName = originalUri.GetLeftPart(UriPartial.Authority); + + var endPoints = await registry.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var results = new List<(string Name, DestinationConfig Config)>(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) + { + var addressString = endPoint.GetEndPointString(); + Uri result; + if (!addressString.Contains("://")) + { + result = new Uri($"https://{addressString}"); + } + else + { + result = new Uri(addressString); + } + + uriBuilder.Host = result.Host; + uriBuilder.Port = result.Port; + var resolvedAddress = uriBuilder.Uri.ToString(); + var healthAddress = originalConfig.Health; + if (healthUriBuilder is not null) + { + healthUriBuilder.Host = result.Host; + healthUriBuilder.Port = result.Port; + healthAddress = healthUriBuilder.Uri.ToString(); + } + + var name = $"{originalName}[{addressString}]"; + var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + results.Add((name, config)); + } + + return (results, endPoints.ChangeToken); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs new file mode 100644 index 00000000000..0da7e8b55eb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -0,0 +1,20 @@ +// 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; +using Microsoft.Extensions.ServiceDiscovery.Http; +using Yarp.ReverseProxy.Forwarder; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp; + +internal sealed class ServiceDiscoveryForwarderHttpClientFactory( + TimeProvider timeProvider, + IServiceEndPointSelectorProvider selectorProvider, + ServiceEndPointResolverFactory factory) : ForwarderHttpClientFactory +{ + protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) + { + var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); + return new ResolvingHttpDelegatingHandler(registry, handler); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs new file mode 100644 index 00000000000..4f511cbf37b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// A service endpoint resolver that uses configuration to resolve endpoints. +/// +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver +{ + private readonly string _serviceName; + private readonly string? _endpointName; + private readonly IConfiguration _configuration; + private readonly IOptions _options; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The configuration. + /// The options. + public ConfigurationServiceEndPointResolver( + string serviceName, + IConfiguration configuration, + IOptions options) + { + if (ServiceNameParts.TryParse(serviceName, out var parts)) + { + _serviceName = parts.Host; + _endpointName = parts.EndPointName; + } + else + { + throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + } + + _configuration = configuration; + _options = options; + } + + /// + public ValueTask DisposeAsync() => default; + + /// + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); + + private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) + { + // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. + if (endPoints.EndPoints.Count != 0) + { + return ResolutionStatus.None; + } + + var root = _configuration; + var baseSectionName = _options.Value.SectionName; + if (baseSectionName is { Length: > 0 }) + { + root = root.GetSection(baseSectionName); + } + + // Get the corresponding config section. + var section = root.GetSection(_serviceName); + if (!section.Exists()) + { + return CreateNotFoundResponse(endPoints, baseSectionName); + } + + // Read the endpoint from the configuration. + // First check if there is a collection of sections + if (section.GetChildren().Any()) + { + var values = section.Get>(); + if (values is { Count: > 0 }) + { + // Use schemes if any of the URIs have a scheme set. + var uris = ParseServiceNameParts(values); + var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + foreach (var uri in uris) + { + // If either schemes are not in-use or the scheme matches, create an endpoint for this value + if (!useSchemes || SchemesMatch(_endpointName, uri)) + { + if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + } + } + } + } + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + { + if (SchemesMatch(_endpointName, uri)) + { + if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + } + } + + endPoints.AddChangeToken(section.GetReloadToken()); + return ResolutionStatus.Success; + + static bool SchemesMatch(string? scheme, ServiceNameParts parts) => + (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) + || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + } + + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + { + var configPath = new StringBuilder(); + if (baseSectionName is { Length: > 0 }) + { + configPath.Append(baseSectionName).Append(':'); + } + + configPath.Append(_serviceName); + endPoints.AddChangeToken(_configuration.GetReloadToken()); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + } + + private static List ParseServiceNameParts(List input) + { + var results = new List(input.Count); + for (var i = 0; i < input.Count; ++i) + { + if (ServiceNameParts.TryParse(input[i], out var value)) + { + results.Add(value); + } + } + + return results; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..a20d12d771c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -0,0 +1,15 @@ +// 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; + +/// +/// Options for . +/// +public class ConfigurationServiceEndPointResolverOptions +{ + /// + /// The name of the configuration section which contains service endpoints. Defaults to "Services". + /// + public string? SectionName { get; set; } = "Services"; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..5c35b6161fd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -0,0 +1,28 @@ +// 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.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// implementation that resolves services using . +/// +/// The configuration. +/// The options. +public class ConfigurationServiceEndPointResolverProvider( + IConfiguration configuration, + IOptions options) : IServiceEndPointResolverProvider +{ + private readonly IConfiguration _configuration = configuration; + private readonly IOptions _options = options; + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs new file mode 100644 index 00000000000..53e4c1f5bda --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -0,0 +1,83 @@ +// 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.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.Internal; + +namespace Microsoft.Extensions.Hosting; + +/// +/// Extension methods for configuring service discovery. +/// +public static class HostingExtensions +{ + /// + /// Adds the core service discovery services and configures defaults. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) + { + return services.AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .AddPassThroughServiceEndPointResolver(); + } + + /// + /// Adds the core service discovery services. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) + { + services.AddOptions(); + services.AddLogging(); + services.TryAddSingleton(static sp => TimeProvider.System); + services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); + return services; + } + + /// + /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + { + return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + } + + /// + /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// + /// The delegate used to configure the provider. + /// The service collection. + /// The service collection. + public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + var options = services.AddOptions(); + configureOptions?.Invoke(options); + return services; + } + + /// + /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// + /// The service collection. + /// The service collection. + public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + { + services.AddServiceDiscoveryCore(); + services.AddSingleton(); + return services; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs new file mode 100644 index 00000000000..7e9df30b770 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -0,0 +1,110 @@ +// 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.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; + +/// +/// Extensions for configuring with service discovery. +/// +public static class HttpClientBuilderExtensions +{ + /// + /// 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); + return new ResolvingHttpDelegatingHandler(registry); + }); + + return httpClientBuilder; + } + + /// + /// Adds service discovery to the . + /// + /// The builder. + /// The builder. + public static IHttpClientBuilder UseServiceDiscovery(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); + return new ResolvingHttpDelegatingHandler(registry); + }); + + // 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; + } + + private static void AddDisableGrpcLoadBalancingFilter(IServiceCollection services, string? name) + { + // A filter is used because it will always run last. This is important because the disable + // property needs to be added to all SocketsHttpHandler instances, including those specified + // with ConfigurePrimaryHttpMessageHandler. + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + services.Configure(o => o.ClientNames.Add(name)); + } + + private sealed class DisableGrpcLoadBalancingFilterOptions + { + // Names of clients. A null value means it is applied globally to all clients. + public HashSet ClientNames { get; } = new HashSet(); + } + + private sealed class DisableGrpcLoadBalancingFilter : IHttpMessageHandlerBuilderFilter + { + private readonly DisableGrpcLoadBalancingFilterOptions _options; + private readonly bool _global; + + public DisableGrpcLoadBalancingFilter(IOptions options) + { + _options = options.Value; + _global = _options.ClientNames.Contains(null); + } + + public Action Configure(Action next) + { + return (builder) => + { + // Run other configuration first, we want to decorate. + next(builder); + if (_global || _options.ClientNames.Contains(builder.Name)) + { + if (builder.PrimaryHandler is SocketsHttpHandler socketsHttpHandler) + { + // gRPC knows about this property and uses it to check whether + // load balancing is disabled when the GrpcChannel is created. + socketsHttpHandler.Properties["__GrpcLoadBalancingDisabled"] = true; + } + } + }; + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs new file mode 100644 index 00000000000..e9e80bc76bb --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -0,0 +1,258 @@ +// 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.Concurrent; +using System.Diagnostics; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// Resolves endpoints for HTTP requests. +/// +public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, IServiceEndPointSelectorProvider selectorProvider, 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 _resolverProvider = resolverProvider; + private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + + /// + /// Resolves and returns a service endpoint for the specified request. + /// + /// The request message. + /// The cancellation token. + /// The resolved service endpoint. + /// The request had no set or a suitable endpoint could not be found. + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + if (request.RequestUri is null) + { + throw new InvalidOperationException("Cannot resolve an endpoint for a request which has no RequestUri"); + } + + EnsureCleanupTimerStarted(); + + var key = request.RequestUri.GetLeftPart(UriPartial.Authority); + while (true) + { + var resolver = _resolvers.GetOrAdd( + key, + static (name, self) => self.CreateResolver(name), + this); + + var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + if (valid) + { + if (endPoint is null) + { + throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); + } + + return endPoint; + } + } + } + + private void EnsureCleanupTimerStarted() + { + if (_cleanupTimer is not null) + { + return; + } + + lock (_lock) + { + if (_cleanupTimer is not null) + { + return; + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + + foreach (var resolver in _resolvers) + { + await resolver.Value.DisposeAsync().ConfigureAwait(false); + } + + _resolvers.Clear(); + if (_cleanupTask is not null) + { + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private void CleanupResolvers() + { + lock (_lock) + { + if (_cleanupTask is { IsCompleted: true }) + { + _cleanupTask = CleanupResolversAsyncCore(); + } + } + } + + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) + { + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } + } + if (cleanupTasks is not null) + { + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); + } + } + + private ResolverEntry CreateResolver(string serviceName) + { + var resolver = _resolverProvider.CreateResolver(serviceName); + var selector = _selectorProvider.CreateSelector(); + var result = new ResolverEntry(resolver, selector); + resolver.Start(); + return result; + } + + private sealed class ResolverEntry : IAsyncDisposable + { + private readonly ServiceEndPointResolver _resolver; + private readonly IServiceEndPointSelector _selector; + private const ulong CountMask = unchecked((ulong)-1); + private const ulong RecentUseFlag = 1UL << 61; + private const ulong DisposingFlag = 1UL << 62; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public ResolverEntry(ServiceEndPointResolver resolver, IServiceEndPointSelector selector) + { + _resolver = resolver; + _selector = selector; + _resolver.OnEndPointsUpdated += result => + { + if (result.ResolvedSuccessfully) + { + _selector.SetEndPoints(result.EndPoints); + } + }; + } + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() + { + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); + + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } + + public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + { + try + { + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndPoint(context); + return (true, result); + } + else + { + return (false, default); + } + } + finally + { + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) + { + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); + } + + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } + } + + private async Task DisposeAsyncCore() + { + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs new file mode 100644 index 00000000000..1edf4cf4898 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -0,0 +1,45 @@ +// 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.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// which resolves endpoints using service discovery. +/// +public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver) : HttpClientHandler +{ + private readonly HttpServiceEndPointResolver _resolver = resolver; + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var originalUri = request.RequestUri; + IEndPointHealthFeature? epHealth = null; + Exception? error = null; + var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + epHealth = result.Features.Get(); + } + + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + error = exception; + throw; + } + finally + { + epHealth?.ReportHealth(responseDuration.Elapsed, 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs new file mode 100644 index 00000000000..b8e0368c5e3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -0,0 +1,99 @@ +// 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.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Http; + +/// +/// HTTP message handler which resolves endpoints using service discovery. +/// +public class ResolvingHttpDelegatingHandler : DelegatingHandler +{ + private readonly HttpServiceEndPointResolver _resolver; + + /// + /// Initializes a new instance. + /// + /// The endpoint resolver. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver) + { + _resolver = resolver; + } + + /// + /// Initializes a new instance. + /// + /// The endpoint resolver. + /// The inner handler. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, HttpMessageHandler innerHandler) : base(innerHandler) + { + _resolver = resolver; + } + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var originalUri = request.RequestUri; + IEndPointHealthFeature? epHealth = null; + Exception? error = null; + var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + if (originalUri?.Host is not null) + { + var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); + request.RequestUri = GetUriWithEndPoint(originalUri, result); + epHealth = result.Features.Get(); + } + + try + { + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + error = exception; + throw; + } + finally + { + epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + request.RequestUri = originalUri; + } + } + + internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint) + { + var endpoint = serviceEndPoint.EndPoint; + + string host; + int port; + switch (endpoint) + { + case IPEndPoint ip: + host = ip.Address.ToString(); + port = ip.Port; + break; + case DnsEndPoint dns: + host = dns.Host; + port = dns.Port; + break; + default: + throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + } + + var builder = new UriBuilder(uri) + { + Host = host, + }; + + // Default to the default port for the scheme. + if (port > 0) + { + builder.Port = port; + } + + return builder.Uri; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..15e566c5887 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,45 @@ +// 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 System.Net; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +/// +/// Service endpoint resolver provider which passes through the provided value. +/// +internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +{ + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) + { + // Propagate the value through regardless, leaving it to the caller to interpret it. + endPoint = new DnsEndPoint(serviceName, 0); + } + + resolver = new PassThroughServiceEndPointResolver(endPoint); + return true; + } + + private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver + { + private readonly EndPoint _endPoint = endPoint; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + if (endPoints.EndPoints.Count != 0) + { + return new(ResolutionStatus.None); + } + + endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs new file mode 100644 index 00000000000..8b2a03acd31 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.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; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Internal; + +internal readonly struct ServiceNameParts +{ + public ServiceNameParts(string host, string? endPointName, int port) : this() + { + Host = host; + EndPointName = endPointName; + Port = port; + } + + public string? EndPointName { get; init; } + + public string Host { get; init; } + + public int Port { get; init; } + + public static 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; + + static ServiceNameParts Create(Uri uri, bool hasScheme) + { + 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; + if (hasScheme) + { + endPointName = uri.Scheme; + } + } + + return new(host, endPointName, uri.Port); + } + } + + public static bool TryCreateEndPoint(ServiceNameParts parts, [NotNullWhen(true)] out EndPoint? endPoint) + { + if (IPAddress.TryParse(parts.Host, out var ip)) + { + endPoint = new IPEndPoint(ip, parts.Port); + } + else if (!string.IsNullOrEmpty(parts.Host)) + { + endPoint = new DnsEndPoint(parts.Host, parts.Port); + } + else + { + endPoint = null; + return false; + } + + return true; + } + + public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + { + if (TryParse(serviceName, out var parts)) + { + return TryCreateEndPoint(parts, out serviceEndPoint); + } + + serviceEndPoint = null; + return false; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs new file mode 100644 index 00000000000..9395896e520 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.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. + +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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..d3f657c9550 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs new file mode 100644 index 00000000000..e233dfb7b5c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs @@ -0,0 +1,50 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..00832bc7811 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs new file mode 100644 index 00000000000..8e4bb2378d8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.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. + +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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..ae74b4032bc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs new file mode 100644 index 00000000000..5848c7d8f72 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.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. + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. +/// +public class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +{ + private uint _next; + 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[(int)(Interlocked.Increment(ref _next) % collection.Count)]; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs new file mode 100644 index 00000000000..ed1e79d7416 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs @@ -0,0 +1,18 @@ +// 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 RandomServiceEndPointSelectorProvider Instance { get; } = new(); + + /// + public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj new file mode 100644 index 00000000000..916145964f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -0,0 +1,16 @@ + + + + $(NetCurrent) + true + + + + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs new file mode 100644 index 00000000000..3b7169d8854 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -0,0 +1,358 @@ +// 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.Runtime.ExceptionServices; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Resolves endpoints for a specified service. +/// +public sealed class ServiceEndPointResolver( + IServiceEndPointResolver[] resolvers, + ILogger logger, + string serviceName, + TimeProvider timeProvider, + IOptions options) : IAsyncDisposable +{ + private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointResolver)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 IServiceEndPointResolver[] _resolvers = resolvers; + private readonly CancellationTokenSource _disposalCancellation = new(); + private ITimer? _pollingTimer; + private ServiceEndPointCollection? _cachedEndPoints; + private Task _refreshTask = Task.CompletedTask; + private volatile CacheStatus _cacheState; + + /// + /// Gets the service name. + /// + public string ServiceName { get; } = serviceName; + + /// + /// Gets or sets the action called when endpoints are updated. + /// + public Action? OnEndPointsUpdated { get; set; } + + /// + /// Starts the endpoint resolver. + /// + public void Start() + { + _ = RefreshAsync(force: false); + } + + /// + /// Returns a collection of resolved endpoints for the service. + /// + /// The cancellation token. + /// A collection of resolved endpoints for the service. + public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + { + // If the cache is valid, return the cached value. + if (_cachedEndPoints is { ChangeToken.HasChanged: false } 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) + { + ServiceEndPointCollection? result; + do + { + await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); + result = _cachedEndPoints; + } while (result is null); + return result; + } + } + + // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation + private Task RefreshAsync(bool force) + { + lock (_lock) + { + // If the cache is invalid or needs invalidation, refresh the cache. + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + { + // Indicate that the cache is being updated and start a new refresh task. + _cacheState = CacheStatus.Refreshing; + + // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _refreshTask = RefreshAsyncInternal(); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + + return _refreshTask; + } + } + + private async Task RefreshAsyncInternal() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var cancellationToken = _disposalCancellation.Token; + Exception? error = null; + ServiceEndPointCollection? newEndPoints = null; + CacheStatus newCacheState; + ResolutionStatus status = ResolutionStatus.Success; + while (true) + { + try + { + var collection = new ServiceEndPointCollectionSource(ServiceName, new FeatureCollection()); + status = ResolutionStatus.Success; + foreach (var resolver in _resolvers) + { + var resolverStatus = await resolver.ResolveAsync(collection, cancellationToken).ConfigureAwait(false); + status = CombineStatus(status, resolverStatus); + } + + var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); + + var statusCode = status.StatusCode; + if (statusCode != ResolutionStatusCode.Success) + { + if (statusCode is ResolutionStatusCode.Pending) + { + // Wait until a timeout or the collection's ChangeToken.HasChange becomes true and try again. + await WaitForPendingChangeToken(endPoints.ChangeToken, _options.PendingStatusRefreshPeriod, cancellationToken).ConfigureAwait(false); + continue; + } + else if (statusCode is ResolutionStatusCode.Cancelled) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception ?? new OperationCanceledException(); + break; + } + else if (statusCode is ResolutionStatusCode.Error) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception; + break; + } + } + + lock (_lock) + { + // 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 => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: false), this); + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + else + { + SchedulePollingTimer(); + } + + // The cache is valid + newEndPoints = (ServiceEndPointCollection?)endPoints; + newCacheState = CacheStatus.Valid; + break; + } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); + status = CombineStatus(status, ResolutionStatus.FromException(exception)); + break; + } + } + + // If there was an error, the cache must be invalid. + Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + + lock (_lock) + { + if (newCacheState is CacheStatus.Valid) + { + Debug.Assert(newEndPoints is not null); + _cachedEndPoints = newEndPoints; + } + + _cacheState = newCacheState; + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + + if (error is not null) + { + _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + ExceptionDispatchInfo.Throw(error); + } + else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + { + _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + } + } + + private void SchedulePollingTimer() + { + lock (_lock) + { + if (_pollingTimer is null) + { + _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + } + else + { + _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + } + } + } + + 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() + { + lock (_lock) + { + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + + _disposalCancellation.Cancel(); + if (_refreshTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + foreach (var resolver in _resolvers) + { + await resolver.DisposeAsync().ConfigureAwait(false); + } + } + + private enum CacheStatus + { + Invalid, + Refreshing, + 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(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs new file mode 100644 index 00000000000..6feb04f3350 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -0,0 +1,56 @@ +// 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.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates instances. +/// +public class ServiceEndPointResolverFactory( + IEnumerable resolvers, + ILogger resolverLogger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers + .Where(r => r is not PassThroughServiceEndPointResolverProvider) + .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); + private readonly ILogger _resolverLogger = resolverLogger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a instance for the provided service name. + /// + public ServiceEndPointResolver CreateResolver(string serviceName) + { + ArgumentNullException.ThrowIfNull(serviceName); + + List? resolvers = null; + foreach (var factory in _resolverProviders) + { + if (factory.TryCreateResolver(serviceName, out var resolver)) + { + resolvers ??= new(); + resolvers.Add(resolver); + } + } + + if (resolvers is not { Count: > 0 }) + { + throw new InvalidOperationException("No resolver which supports the provided service name has been configured"); + } + + return new ServiceEndPointResolver( + resolvers: resolvers.ToArray(), + logger: _resolverLogger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..8a2b37a5048 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.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.Primitives; + +namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; + +/// +/// Options for . +/// +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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs new file mode 100644 index 00000000000..8d039f2d74d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs @@ -0,0 +1,240 @@ +// 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.Concurrent; +using System.Diagnostics; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Resolves service names to collections of endpoints. +/// +/// The resolver factory. +/// The time provider. +public sealed class ServiceEndPointResolverRegistry(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) : IAsyncDisposable +{ + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolverRegistry)s!).CleanupResolvers(); + private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); + + private readonly object _lock = new(); + private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + private bool _disposed; + + /// + /// Resolves and returns service endpoints for the specified service. + /// + /// The service name. + /// The cancellation token. + /// The resolved service endpoints. + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(serviceName); + ObjectDisposedException.ThrowIf(_disposed, this); + + EnsureCleanupTimerStarted(); + + while (true) + { + ObjectDisposedException.ThrowIf(_disposed, this); + var resolver = _resolvers.GetOrAdd( + serviceName, + static (name, self) => self.CreateResolver(name), + this); + + var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + if (valid) + { + if (result is null) + { + throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); + } + + return result; + } + } + } + + private void EnsureCleanupTimerStarted() + { + if (_cleanupTimer is not null) + { + return; + } + + lock (_lock) + { + if (_cleanupTimer is not null) + { + return; + } + + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); + } + finally + { + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + } + + /// + public async ValueTask DisposeAsync() + { + lock (_lock) + { + _disposed = true; + _cleanupTimer?.Dispose(); + _cleanupTimer = null; + } + + foreach (var resolver in _resolvers) + { + await resolver.Value.DisposeAsync().ConfigureAwait(false); + } + + _resolvers.Clear(); + if (_cleanupTask is not null) + { + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } + + private void CleanupResolvers() + { + lock (_lock) + { + if (_cleanupTask is { IsCompleted: true }) + { + _cleanupTask = CleanupResolversAsyncCore(); + } + } + } + + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) + { + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } + } + if (cleanupTasks is not null) + { + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); + } + } + + private ResolverEntry CreateResolver(string serviceName) + { + var resolver = _resolverProvider.CreateResolver(serviceName); + resolver.Start(); + return new ResolverEntry(resolver); + } + + private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable + { + private readonly ServiceEndPointResolver _resolver = resolver; + private const ulong CountMask = unchecked((ulong)-1); + private const ulong RecentUseFlag = 1UL << 61; + private const ulong DisposingFlag = 1UL << 62; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() + { + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); + + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } + + public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + { + try + { + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endPoints); + } + else + { + return (false, default); + } + } + finally + { + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } + } + } + + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) + { + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); + } + + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } + } + + private async Task DisposeAsyncCore() + { + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..1b3a20fed19 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs @@ -0,0 +1,285 @@ +// 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; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +/// +/// Tests for and . +/// These also cover and by extension. +/// +public class DnsServiceEndPointResolverTests +{ + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } + + [Fact] + public async Task ResolveServiceEndPoint_Dns() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + } + }; + + return Task.FromResult(response); + } + }; + var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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(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); + } + } + + /// + /// Tests that when there are multiple resolvers registered, they are consulted in registration order and each provider only adds endpoints if the providers before it did not. + /// + [InlineData(true)] + [InlineData(false)] + [Theory] + public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + } + }; + + return Task.FromResult(response); + } + }; + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "localhost:8080", + ["services:basket:1"] = "remotehost:9090", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var serviceCollection = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore(); + if (dnsFirst) + { + serviceCollection + .AddDnsSrvServiceEndPointResolver() + .AddConfigurationServiceEndPointResolver(); + } + else + { + serviceCollection + .AddConfigurationServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(); + } + var services = serviceCollection.BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.Null(initialResult.Status.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(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); + } + 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); + } + } + } + + public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; + public void SetValues(IEnumerable> values) + { + Data.Clear(); + foreach (var (key, value) in values) + { + Data[key] = value; + } + + OnReload(); + } + } + + /* + [Fact] + public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() + { + var oneEndPoint = new Dictionary + { + ["services:basket:http:0:host"] = "localhost", + ["services:basket:http:0:port"] = "8080", + }; + var bothEndPoints = new Dictionary(oneEndPoint) + { + ["services:basket:http:1:host"] = "remotehost", + ["services:basket:http:1:port"] = "9090", + }; + var configSource = new MyConfigurationProvider(); + var services = new ServiceCollection() + .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) + .AddServiceDiscovery() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var channel = Channel.CreateUnbounded(); + resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); + resolver.Start(); + var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.False(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); + Assert.Null(initialResult.EndPoints); + + // Update the config and check that it flows through the system. + configSource.SetValues(oneEndPoint); + + // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to + // cause an indefinite test hang. We expect the result to be published practically immediately, though. + _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + var firstEp = Assert.Single(oneEpResult); + Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); + + // Do it again to check that an updated (not cached) version is published. + configSource.SetValues(bothEndPoints); + var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + Assert.True(twoEpResult.ResolvedSuccessfully); + Assert.Equal(2, twoEpResult.EndPoints.Count); + Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); + } + } + */ +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj new file mode 100644 index 00000000000..ba827640199 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -0,0 +1,20 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..5b0ec89d7a1 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -0,0 +1,135 @@ +// 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; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Memory; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for and . +/// These also cover and by extension. +/// +public class ConfigurationServiceEndPointResolverTests +{ + [Fact] + public async Task ResolveServiceEndPoint_Configuration_SingleResult() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket"] = "localhost:8080", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + ["services:basket:1"] = "http://remotehost:9090", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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 DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + ["services:basket:1"] = "http://remotehost:9090", + ["services:basket:2"] = "http://_grpc.localhost:2222", + ["services:basket:3"] = "grpc://remotehost:2222", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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 DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + } + } + + public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) => this; + public void SetValues(IEnumerable> values) + { + Data.Clear(); + foreach (var (key, value) in values) + { + Data[key] = value; + } + + OnReload(); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj new file mode 100644 index 00000000000..73bc9ec1eab --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -0,0 +1,19 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..3ede6371deb --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -0,0 +1,112 @@ +// 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; +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 Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for . +/// These also cover and by extension. +/// +public class PassThroughServiceEndPointResolverTests +{ + [Fact] + public async Task ResolveServiceEndPoint_PassThrough() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Superseded() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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 DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Fallback() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs new file mode 100644 index 00000000000..7cbfbada0fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -0,0 +1,192 @@ +// 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 System.Net; +using System.Threading.Channels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +/// +/// Tests for and . +/// +public class ServiceEndPointResolverTests +{ + [Fact] + public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + } + + [Fact] + public void ResolveServiceEndPoint_NullServiceName_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + Assert.Throws(() => resolverFactory.CreateResolver(null!)); + } + + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider + { + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + { + bool result; + (result, resolver) = createResolverDelegate(serviceName); + return result; + } + } + + private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver + { + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask DisposeAsync() => disposeAsync(); + } + + [Fact] + public async Task ResolveServiceEndPoint() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(resolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("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 ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + Assert.False(tcs.Task.IsCompleted); + + 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(); + 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]); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_ThrowOnReload() + { + var sem = new SemaphoreSlim(0); + var cts = new[] { new CancellationTokenSource() }; + var throwOnNextResolve = new[] { false }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: async (collection, ct) => + { + await sem.WaitAsync(ct).ConfigureAwait(false); + if (cts[0].IsCancellationRequested) + { + // Always be sure to have a fresh token. + cts[0] = new(); + } + + if (throwOnNextResolve[0]) + { + throwOnNextResolve[0] = false; + throw new InvalidOperationException("throwing"); + } + + 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) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + + ServiceEndPointResolver resolver; + await using ((resolver = resolverFactory.CreateResolver("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); + + // Tell the resolver to throw on the next resolve call and then trigger a reload. + throwOnNextResolve[0] = true; + cts[0].Cancel(); + + var exception = await Assert.ThrowsAsync(async () => + { + var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + sem.Release(1); + await resolveTask.ConfigureAwait(false); + }).ConfigureAwait(false); + + Assert.Equal("throwing", exception.Message); + + var channel = Channel.CreateUnbounded(); + resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + + do + { + cts[0].Cancel(); + sem.Release(1); + var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + await resolveTask.ConfigureAwait(false); + var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + if (next.ResolvedSuccessfully) + { + break; + } + } while (true); + + 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 ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + } + } +} From 727e5ea2e04d900c9a1111b870d2d2d87a4cdd03 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Thu, 28 Sep 2023 15:35:57 -0700 Subject: [PATCH 02/77] False positive. **BYPASS_SECRET_SCANNING** From ec97849b6aea76ef3e2468825f5669fbc53aafe1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 17 Oct 2023 13:24:00 -0700 Subject: [PATCH 03/77] Service Discovery: fail when no resolvers have been registered (#340) --- .../ServiceEndPointResolver.cs | 18 ++++++++++-- .../ServiceEndPointResolverFactory.cs | 2 +- .../ServiceEndPointResolverTests.cs | 29 ++++++++++++++++++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 3b7169d8854..a1ee606b688 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.ExceptionServices; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.Logging; @@ -49,6 +50,7 @@ public sealed class ServiceEndPointResolver( /// public void Start() { + ThrowIfNoResolvers(); _ = RefreshAsync(force: false); } @@ -59,6 +61,8 @@ public void Start() /// A collection of resolved endpoints for the service. public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { + ThrowIfNoResolvers(); + // If the cache is valid, return the cached value. if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) { @@ -138,7 +142,6 @@ private async Task RefreshAsyncInternal() } var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; if (statusCode != ResolutionStatusCode.Success) { @@ -181,7 +184,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = (ServiceEndPointCollection?)endPoints; + newEndPoints = endPoints; newCacheState = CacheStatus.Valid; break; } @@ -355,4 +358,15 @@ private static async Task WaitForPendingChangeToken(IChangeToken changeToken, Ti timerCancellation?.Dispose(); } } + + private void ThrowIfNoResolvers() + { + if (_resolvers.Length == 0) + { + ThrowNoResolversConfigured(); + } + } + + [DoesNotReturn] + private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 6feb04f3350..94696ceb413 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -43,7 +43,7 @@ public ServiceEndPointResolver CreateResolver(string serviceName) if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured"); + throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); } return new ServiceEndPointResolver( diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 7cbfbada0fa..aa91a41f887 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -6,6 +6,7 @@ using System.Threading.Channels; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Xunit; @@ -24,7 +25,21 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .AddServiceDiscoveryCore() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); + Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + } + + [Fact] + public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + { + var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var resolverFactory = new ServiceEndPointResolver([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var exception = Assert.Throws(resolverFactory.Start); + Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); + Assert.Equal("No service endpoint resolvers are configured.", exception.Message); } [Fact] @@ -37,6 +52,18 @@ public void ResolveServiceEndPoint_NullServiceName_Throws() Assert.Throws(() => resolverFactory.CreateResolver(null!)); } + [Fact] + public async Task UseServiceDiscovery_NoResolvers_Throws() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("foo", c => c.BaseAddress = new("http://foo")) + .UseServiceDiscovery(); + 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 has been configured.", exception.Message); + } + private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider { public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) From 2bb9a6ee2308f0c240ce5e8222ab9784f3bbd359 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 23 Oct 2023 13:14:42 -0700 Subject: [PATCH 04/77] Service Discovery: Refactor DNS & DNS SRV providers into subclasses (#343) * Service Discovery: Allow DNS and DNS SRV to be added independently --- .../DnsServiceEndPointResolver.cs | 280 ++---------------- ... => DnsServiceEndPointResolverBase.Log.cs} | 14 +- .../DnsServiceEndPointResolverBase.cs | 196 ++++++++++++ .../DnsServiceEndPointResolverOptions.cs | 13 - .../DnsServiceEndPointResolverProvider.cs | 138 +-------- .../DnsSrvServiceEndPointResolver.cs | 82 +++++ .../DnsSrvServiceEndPointResolverOptions.cs | 38 +++ .../DnsSrvServiceEndPointResolverProvider.cs | 150 ++++++++++ .../HostingExtensions.cs | 31 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 10 +- ... => DnsSrvServiceEndPointResolverTests.cs} | 12 +- 11 files changed, 545 insertions(+), 419 deletions(-) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.Log.cs => DnsServiceEndPointResolverBase.Log.cs} (73%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsServiceEndPointResolverTests.cs => DnsSrvServiceEndPointResolverTests.cs} (97%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 63d60d5318d..4b5783cd0d5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -1,282 +1,42 @@ // 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 DnsClient; -using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// A service end point resolver that uses DNS to resolve the service end points. -/// -internal sealed partial class DnsServiceEndPointResolver : IServiceEndPointResolver +internal sealed partial class DnsServiceEndPointResolver( + string serviceName, + string hostName, + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) { - private readonly object _lock = new(); - private readonly string _serviceName; - private readonly Stopwatch _lastRefreshTimer = new(); - private readonly IOptionsMonitor _options; - private readonly ILogger _logger; - private readonly CancellationTokenSource _disposeCancellation = new(); - private readonly IDnsQuery _dnsClient; - private readonly TimeProvider _timeProvider; - private Task _resolveTask = Task.CompletedTask; - private ResolutionStatus _lastStatus; - private IChangeToken? _lastChangeToken; - private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; - private readonly string _addressRecordName; - private readonly string _srvRecordName; - private readonly int _defaultPort; - private TimeSpan _nextRefreshPeriod; + protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; - /// - /// Initializes a new instance. - /// - /// The service name. - /// The name used to resolve the address of this service. - /// The name used to resolve this service's SRV record in DNS. - /// The default port to use for endpoints. - /// The options. - /// The logger. - /// The DNS client. - /// The time provider. - public DnsServiceEndPointResolver( - string serviceName, - string addressRecordName, - string srvRecordName, - int defaultPort, - IOptionsMonitor options, - ILogger logger, - IDnsQuery dnsClient, - TimeProvider timeProvider) - { - _serviceName = serviceName; - _options = options; - _logger = logger; - _lastEndPointCollection = null; - _addressRecordName = addressRecordName; - _srvRecordName = srvRecordName; - _defaultPort = defaultPort; - _dnsClient = dnsClient; - _nextRefreshPeriod = _options.CurrentValue.MinRetryPeriod; - _timeProvider = timeProvider; - var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(_options.CurrentValue.DefaultRefreshPeriod); - _lastChangeToken = new CancellationChangeToken(cancellation.Token); - } - - /// - public async ValueTask ResolveAsync(ServiceEndPointCollectionSource 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) - { - return ResolutionStatus.None; - } - - if (ShouldRefresh()) - { - Task resolveTask; - lock (_lock) - { - if (_resolveTask.IsCompleted && ShouldRefresh()) - { - _resolveTask = ResolveAsyncInternal(); - } - - resolveTask = _resolveTask; - } - - await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); - } - - lock (_lock) - { - if (_lastEndPointCollection is { Count: > 0 } eps) - { - foreach (var ep in eps) - { - endPoints.EndPoints.Add(ep); - } - } - - if (_lastChangeToken is not null) - { - endPoints.AddChangeToken(_lastChangeToken); - } - - return _lastStatus; - } - } - - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || _lastRefreshTimer.Elapsed >= _nextRefreshPeriod; - - private async Task ResolveAsyncInternal() + protected override async Task ResolveAsyncCore() { var endPoints = new List(); - var options = _options.CurrentValue; - var ttl = options.DefaultRefreshPeriod; - try + var ttl = DefaultRefreshPeriod; + Log.AddressQuery(logger, ServiceName, hostName); + var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + foreach (var address in addresses) { - if (options.UseSrvQuery) - { - Log.SrvQuery(_logger, _serviceName, _srvRecordName); - var result = await _dnsClient.QueryAsync(_srvRecordName, QueryType.SRV).ConfigureAwait(false); - if (result.HasError) - { - SetException(CreateException(result.ErrorMessage), ttl); - return; - } - - var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals) - { - ttl = MinTtl(record, ttl); - lookupMapping[record.DomainName] = record; - } - - var srvRecords = result.Answers.OfType(); - foreach (var record in srvRecords) - { - if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) - { - continue; - } - - ttl = MinTtl(record, ttl); - if (targetRecord is AddressRecord addressRecord) - { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); - } - else if (targetRecord is CNameRecord canonicalNameRecord) - { - endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); - } - } - } - else - { - Log.AddressQuery(_logger, _serviceName, _addressRecordName); - var addresses = await System.Net.Dns.GetHostAddressesAsync(_addressRecordName, _disposeCancellation.Token).ConfigureAwait(false); - foreach (var address in addresses) - { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, _defaultPort))); - } - - if (endPoints.Count == 0) - { - SetException(CreateException(), ttl); - return; - } - } - - SetResult(endPoints, ttl); - } - catch (Exception exception) - { - SetException(exception, ttl); - throw; + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, 0))); } - static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + if (endPoints.Count == 0) { - var candidate = TimeSpan.FromSeconds(record.TimeToLive); - return candidate < existing ? candidate : existing; + SetException(new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName}).")); + return; } - InvalidOperationException CreateException(string? errorMessage = null) - { - var msg = errorMessage switch - { - { Length: > 0 } => $"No DNS records were found for service {_serviceName}: {errorMessage}.", - _ => $"No DNS records were found for service {_serviceName}." - }; - var exception = new InvalidOperationException(msg); - return exception; - } - } - - private void SetException(Exception exception, TimeSpan validityPeriod) => SetResult(endPoints: null, exception, validityPeriod); - private void SetResult(List endPoints, TimeSpan validityPeriod) => SetResult(endPoints, exception: null, validityPeriod); - private void SetResult(List? endPoints, Exception? exception, TimeSpan validityPeriod) - { - lock (_lock) - { - var options = _options.CurrentValue; - if (exception is not null) - { - if (_lastStatus.Exception is null) - { - _nextRefreshPeriod = options.MinRetryPeriod; - } - else - { - var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * options.RetryBackOffFactor)); - _nextRefreshPeriod = nextPeriod > options.MaxRetryPeriod ? options.MaxRetryPeriod : nextPeriod; - } - - 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); - } - } - else - { - _lastRefreshTimer.Restart(); - _nextRefreshPeriod = options.DefaultRefreshPeriod; - _lastStatus = ResolutionStatus.Success; - } - - validityPeriod = validityPeriod > TimeSpan.Zero && validityPeriod < _nextRefreshPeriod ? validityPeriod : _nextRefreshPeriod; - _lastCollectionCancellation.Cancel(); - var cancellation = _lastCollectionCancellation = CreateCancellationTokenSource(validityPeriod); - _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; - } - - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, _serviceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, _serviceName); - } - } - - /// - public async ValueTask DisposeAsync() - { - _disposeCancellation.Cancel(); - - if (_resolveTask is { } task) - { - await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - } - } - - private CancellationTokenSource CreateCancellationTokenSource(TimeSpan validityPeriod) - { - if (validityPeriod <= TimeSpan.Zero) - { - // Do not invalidate on a timer, but invalidate on refresh. - return new CancellationTokenSource(); - } - else - { - return new CancellationTokenSource(validityPeriod, _timeProvider); - } + SetResult(endPoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 6ace4ac983e..81668041ae1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal partial class DnsServiceEndPointResolver +internal partial class DnsServiceEndPointResolverBase { - private static partial class Log + internal static partial class Log { [LoggerMessage(1, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using DNS SRV lookup for name '{RecordName}'.", EventName = "SrvQuery")] public static partial void SrvQuery(ILogger logger, string serviceName, string recordName); @@ -31,10 +31,16 @@ public static void DiscoveredEndPoints(ILogger logger, List end [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] + [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - [LoggerMessage(4, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] + [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + + [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); + + [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs new file mode 100644 index 00000000000..63f649531e8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -0,0 +1,196 @@ +// 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.Logging; +using Microsoft.Extensions.Primitives; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// A service end point resolver that uses DNS to resolve the service end points. +/// +internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointResolver +{ + private readonly object _lock = new(); + private readonly ILogger _logger; + private readonly CancellationTokenSource _disposeCancellation = new(); + private readonly TimeProvider _timeProvider; + private long _lastRefreshTimeStamp; + private Task _resolveTask = Task.CompletedTask; + private ResolutionStatus _lastStatus; + private CancellationChangeToken _lastChangeToken; + private CancellationTokenSource _lastCollectionCancellation; + private List? _lastEndPointCollection; + private TimeSpan _nextRefreshPeriod; + + /// + /// Initializes a new instance. + /// + /// The service name. + /// The logger. + /// The time provider. + public DnsServiceEndPointResolverBase( + string serviceName, + ILogger logger, + TimeProvider timeProvider) + { + ServiceName = serviceName; + _logger = logger; + _lastEndPointCollection = null; + _timeProvider = timeProvider; + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + } + + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); + + protected string ServiceName { get; } + + protected abstract double RetryBackOffFactor { get; } + protected abstract TimeSpan MinRetryPeriod { get; } + protected abstract TimeSpan MaxRetryPeriod { get; } + protected abstract TimeSpan DefaultRefreshPeriod { get; } + protected CancellationToken ShutdownToken => _disposeCancellation.Token; + + /// + public async ValueTask ResolveAsync(ServiceEndPointCollectionSource 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) + { + return ResolutionStatus.None; + } + + if (ShouldRefresh()) + { + Task resolveTask; + lock (_lock) + { + if (_resolveTask.IsCompleted && ShouldRefresh()) + { + _resolveTask = ResolveAsyncInternal(); + } + + resolveTask = _resolveTask; + } + + await resolveTask.WaitAsync(cancellationToken).ConfigureAwait(false); + } + + lock (_lock) + { + if (_lastEndPointCollection is { Count: > 0 } eps) + { + foreach (var ep in eps) + { + endPoints.EndPoints.Add(ep); + } + } + + endPoints.AddChangeToken(_lastChangeToken); + return _lastStatus; + } + } + + private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + + 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) + { + lock (_lock) + { + if (exception is not null) + { + _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); + } + } + else if (endPoints is not { Count: > 0 }) + { + _nextRefreshPeriod = GetRefreshPeriod(); + validityPeriod = TimeSpan.Zero; + _lastStatus = ResolutionStatus.Pending; + } + else + { + _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); + _nextRefreshPeriod = DefaultRefreshPeriod; + _lastStatus = ResolutionStatus.Success; + } + + if (validityPeriod <= TimeSpan.Zero) + { + validityPeriod = _nextRefreshPeriod; + } + else if (validityPeriod > _nextRefreshPeriod) + { + validityPeriod = _nextRefreshPeriod; + } + + _lastCollectionCancellation.Cancel(); + var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); + _lastChangeToken = new CancellationChangeToken(cancellation.Token); + _lastEndPointCollection = endPoints; + } + + if (exception is null) + { + Debug.Assert(endPoints is not null); + Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); + } + else + { + Log.ResolutionFailed(_logger, exception, ServiceName); + } + + TimeSpan GetRefreshPeriod() + { + if (_lastStatus.StatusCode is ResolutionStatusCode.Success) + { + return MinRetryPeriod; + } + + var nextPeriod = TimeSpan.FromTicks((long)(_nextRefreshPeriod.Ticks * RetryBackOffFactor)); + return nextPeriod > MaxRetryPeriod ? MaxRetryPeriod : nextPeriod; + } + } + + /// + public async ValueTask DisposeAsync() + { + _disposeCancellation.Cancel(); + + if (_resolveTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 45adebfea0b..fee7bf2a245 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -27,17 +27,4 @@ public class DnsServiceEndPointResolverOptions /// Gets or sets the retry period growth factor. /// public double RetryBackOffFactor { get; set; } = 2; - - /// - /// Gets or sets the default DNS namespace for services resolved via this provider. - /// - /// - /// If not specified, the provider will attempt to infer the namespace. - /// - public string? DnsNamespace { get; set; } - - /// - /// Gets or sets a value indicating whether to use DNS SRV queries to discover host addresses and ports. - /// - public bool UseSrvQuery { get; set; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 4a554d7c9e7..fc5f707e411 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -2,148 +2,38 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; -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; /// /// Provides instances which resolve endpoints from DNS. /// -internal sealed partial class DnsServiceEndPointResolverProvider : IServiceEndPointResolverProvider +/// +/// Initializes a new instance. +/// +/// The options. +/// The logger. +/// The time provider. +internal sealed partial class DnsServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndPointResolverProvider { - private readonly IOptionsMonitor _options; - private readonly ILogger _logger; - private readonly IDnsQuery _dnsClient; - private readonly TimeProvider _timeProvider; - private readonly string? _defaultNamespace; - - /// - /// Initializes a new instance. - /// - /// The options. - /// The logger. - /// The DNS client. - /// The time provider. - public DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - IDnsQuery dnsClient, - TimeProvider timeProvider) - { - _options = options; - _logger = logger; - _dnsClient = dnsClient; - _timeProvider = timeProvider; - _defaultNamespace = options.CurrentValue.DnsNamespace ?? GetHostNamespace(); - } - - // RFC 2181 - // DNS hostnames can consist only of letters, digits, dots, and hyphens. - // They must begin with a letter. - // They must end with a letter or a digit. - // Individual segments (between dots) can be no longer than 63 characters. - [GeneratedRegex("^(?![0-9]+$)(?!.*-$)(?!-)[a-zA-Z0-9-]{1,63}$")] - private static partial Regex ValidDnsName(); - - // Adapted version of Tim Berners Lee's regex from the URI spec: https://stackoverflow.com/a/26766402 - // Adapted to parse the port into a group and discard groups which we do not care about. - [GeneratedRegex("^(?:([^:/?#]+)://)?([^/?#:]*)?(?::([\\d]+))?")] - private static partial Regex UriRegex(); - /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - // Kubernetes DNS spec: https://github.com/kubernetes/dns/blob/master/docs/specification.md - // SRV records are available for headless services with named ports. - // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.svc.{zone}" - // We can fetch the namespace from /var/run/secrets/kubernetes.io/serviceaccount/namespace - // 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 dnsServiceName = serviceName; - var dnsNamespace = _defaultNamespace; - var portName = "default"; - var defaultPortNumber = 0; - - // Allow the service name to be expressed as either a URI or a plain DNS name. - var uri = UriRegex().Match(serviceName); - if (uri.Success) - { - if (uri.Groups[1].ValueSpan is { Length: > 0 } uriPortNameSpan) - { - // Override the port name if it was specified in the service name - portName = uriPortNameSpan.ToString(); - } - - if (int.TryParse(uri.Groups[3].ValueSpan, out var uriDefaultPort)) - { - // Override the default port if it was specified in the service name - defaultPortNumber = uriDefaultPort; - } - - // Since the service name was URI-formatted, we should extract the hostname part for resolution. - dnsServiceName = uri.Groups[2].Value; - } - else if (!ValidDnsName().IsMatch(serviceName)) + if (!ServiceNameParts.TryParse(serviceName, out var parts)) { + DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; return false; } - // If the DNS name is not qualified, and we have a qualifier, apply it. - if (!dnsServiceName.Contains('.') && dnsNamespace is not null) - { - dnsServiceName = $"{dnsServiceName}.{dnsNamespace}"; - } - - var srvRecordName = $"_{portName}._tcp.{dnsServiceName}"; - resolver = new DnsServiceEndPointResolver(serviceName, dnsServiceName, srvRecordName, defaultPortNumber, _options, _logger, _dnsClient, _timeProvider); + resolver = new DnsServiceEndPointResolver(serviceName, hostName: parts.Host, options, logger, timeProvider); return true; } - - private static string? GetHostNamespace() => ReadNamespaceFromKubernetesServiceAccount() ?? ReadQualifiedNamespaceFromResolvConf(); - - private static string? ReadNamespaceFromKubernetesServiceAccount() - { - if (OperatingSystem.IsLinux()) - { - // Read the namespace from the Kubernetes pod's service account. - var serviceAccountNamespacePath = Path.Combine($"{Path.DirectorySeparatorChar}var", "run", "secrets", "kubernetes.io", "serviceaccount", "namespace"); - if (File.Exists(serviceAccountNamespacePath)) - { - return File.ReadAllText(serviceAccountNamespacePath).Trim(); - } - } - - return null; - } - - private static string? ReadQualifiedNamespaceFromResolvConf() - { - if (OperatingSystem.IsLinux()) - { - var resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); - if (File.Exists(resolveConfPath)) - { - var lines = File.ReadAllLines(resolveConfPath); - foreach (var line in lines) - { - if (line.StartsWith("search ")) - { - var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); - - if (components.Length > 1) - { - return components[1]; - } - } - } - } - } - - return default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs new file mode 100644 index 00000000000..cb6043f94ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -0,0 +1,82 @@ +// 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; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsSrvServiceEndPointResolver( + string serviceName, + string srvQuery, + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) +{ + protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + + protected override async Task ResolveAsyncCore() + { + var endPoints = new List(); + var ttl = DefaultRefreshPeriod; + Log.SrvQuery(logger, ServiceName, srvQuery); + var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); + if (result.HasError) + { + SetException(CreateException(srvQuery, result.ErrorMessage)); + return; + } + + var lookupMapping = new Dictionary(); + foreach (var record in result.Additionals) + { + ttl = MinTtl(record, ttl); + lookupMapping[record.DomainName] = record; + } + + var srvRecords = result.Answers.OfType(); + foreach (var record in srvRecords) + { + if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) + { + continue; + } + + ttl = MinTtl(record, ttl); + if (targetRecord is AddressRecord addressRecord) + { + endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + } + else if (targetRecord is CNameRecord canonicalNameRecord) + { + endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + } + } + + SetResult(endPoints, ttl); + + static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + { + var candidate = TimeSpan.FromSeconds(record.TimeToLive); + return candidate < existing ? candidate : existing; + } + + InvalidOperationException CreateException(string dnsName, string errorMessage) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", + _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + }; + return new InvalidOperationException(msg); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..83ccd9afdbe --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -0,0 +1,38 @@ +// 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.Dns; + +/// +/// Options for configuring . +/// +public class DnsSrvServiceEndPointResolverOptions +{ + /// + /// Gets or sets the default refresh period for endpoints resolved from DNS. + /// + public TimeSpan DefaultRefreshPeriod { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Gets or sets the initial period between retries. + /// + public TimeSpan MinRetryPeriod { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// Gets or sets the maximum period between retries. + /// + public TimeSpan MaxRetryPeriod { get; set; } = TimeSpan.FromSeconds(30); + + /// + /// Gets or sets the retry period growth factor. + /// + public double RetryBackOffFactor { get; set; } = 2; + + /// + /// Gets or sets the default DNS query suffix for services resolved via this provider. + /// + /// + /// If not specified, the provider will attempt to infer the namespace. + /// + public string? QuerySuffix { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs new file mode 100644 index 00000000000..09d9ddbc556 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -0,0 +1,150 @@ +// 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 DnsClient; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides instances which resolve endpoints from DNS using SRV queries. +/// +/// +/// Initializes a new instance. +/// +/// The options. +/// The logger. +/// The DNS client. +/// The time provider. +internal sealed partial class DnsSrvServiceEndPointResolverProvider( + IOptionsMonitor options, + ILogger logger, + IDnsQuery dnsClient, + TimeProvider timeProvider) : IServiceEndPointResolverProvider +{ + 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"); + private static readonly string s_resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); + private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); + + /// + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? 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 + // SRV records are available for headless services with named ports. + // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.{suffix}" + // The suffix (after the service name) can be parsed from /etc/resolv.conf + // 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". + if (string.IsNullOrWhiteSpace(_querySuffix)) + { + DnsServiceEndPointResolverBase.Log.NoDnsSuffixFound(logger, serviceName); + resolver = default; + return false; + } + + if (!ServiceNameParts.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, options, logger, dnsClient, timeProvider); + return true; + } + + private static string? GetKubernetesHostDomain() + { + // Check that we are running in Kubernetes first. + if (!IsInKubernetesCluster()) + { + return null; + } + + if (!OperatingSystem.IsLinux()) + { + return null; + } + + var qualifiedNamespace = ReadQualifiedNamespaceFromResolvConf(); + if (!string.IsNullOrWhiteSpace(qualifiedNamespace)) + { + return qualifiedNamespace; + } + + var serviceAccountNamespace = ReadNamespaceFromKubernetesServiceAccount(); + if (!string.IsNullOrWhiteSpace(serviceAccountNamespace)) + { + // The zone is assumed to be "cluster.local" + return $"{serviceAccountNamespace}.svc.cluster.local"; + } + + return null; + } + + private static string? ReadNamespaceFromKubernetesServiceAccount() + { + // Read the namespace from the Kubernetes pod's service account. + if (File.Exists(s_serviceAccountNamespacePath)) + { + return File.ReadAllText(s_serviceAccountNamespacePath).Trim(); + } + + return null; + } + + private static string? ReadQualifiedNamespaceFromResolvConf() + { + if (!File.Exists(s_resolveConfPath)) + { + return default; + } + + // See https://manpages.debian.org/bookworm/manpages/resolv.conf.5.en.html#search for the format of /etc/resolv.conf's search option. + // In our case, we are interested in determining the domain name. + var lines = File.ReadAllLines(s_resolveConfPath); + foreach (var line in lines) + { + if (!line.StartsWith("search ")) + { + continue; + } + + var components = line.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); + if (components.Length > 1) + { + return components[1]; + } + } + + return default; + } + + private static bool IsInKubernetesCluster() + { + var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); + var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT"); + if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) + { + return false; + } + + var tokenPath = Path.Combine(s_serviceAccountPath, "token"); + if (!File.Exists(tokenPath)) + { + return false; + } + + var certPath = Path.Combine(s_serviceAccountPath, "ca.crt"); + return File.Exists(certPath); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs index 49351af386d..e385bde69a7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs @@ -4,7 +4,6 @@ using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; using Microsoft.Extensions.ServiceDiscovery.Dns; @@ -16,30 +15,40 @@ namespace Microsoft.Extensions.Hosting; public static class HostingExtensions { /// - /// Adds DNS-based service discovery to the . + /// Adds DNS SRV service discovery to the . /// /// The service collection. - /// The DNS service discovery configuration options. + /// The DNS SRV service discovery configuration options. /// The provided . - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { - services.Configure(options => options.UseSrvQuery = true); - return services.AddDnsServiceEndPointResolver(configureOptions); + services.AddServiceDiscoveryCore(); + services.TryAddSingleton(); + services.AddSingleton(); + var options = services.AddOptions(); + options.Configure(o => configureOptions?.Invoke(o)); + return services; } /// - /// Adds DNS-based service discovery to the . + /// Adds DNS service discovery to the . /// /// The service collection. - /// The DNS service discovery configuration options. + /// The DNS SRV service discovery configuration options. /// The provided . - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action>? configureOptions = null) + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) { services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); services.AddSingleton(); var options = services.AddOptions(); - configureOptions?.Invoke(options); + options.Configure(o => configureOptions?.Invoke(o)); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 7f208a3f84a..05b59e6dee6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,10 +1,14 @@ - + $(NetCurrent) true + + + + @@ -19,4 +23,8 @@ + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs similarity index 97% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 1b3a20fed19..558d7260f52 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . +/// Tests for and . /// These also cover and by extension. /// -public class DnsServiceEndPointResolverTests +public class DnsSrvServiceEndPointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -101,7 +101,7 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; @@ -170,15 +170,15 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver() + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .AddConfigurationServiceEndPointResolver(); } else { serviceCollection .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(); - } + .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + }; var services = serviceCollection.BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; From ea510d7c34bf0fc727b38be749b0d0c7e9bb41dd Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:32:19 -0700 Subject: [PATCH 05/77] Add IHostNameFeature to propagate original host name to HttpClient (#460) --- .../Features/IHostNameFeature.cs | 16 ++++++++++++++ .../DnsServiceEndPointResolver.cs | 8 +++++-- .../DnsServiceEndPointResolverBase.cs | 4 ++-- .../DnsSrvServiceEndPointResolver.cs | 16 +++++++++++--- .../DnsSrvServiceEndPointResolverProvider.cs | 2 +- .../ConfigurationServiceEndPointResolver.cs | 16 +++++++++++--- .../Http/ResolvingHttpClientHandler.cs | 1 + .../Http/ResolvingHttpDelegatingHandler.cs | 1 + .../DnsSrvServiceEndPointResolverTests.cs | 7 +++++++ ...nfigurationServiceEndPointResolverTests.cs | 21 +++++++++++++++++++ 10 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs new file mode 100644 index 00000000000..fff3c3fa3f8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// Exposes the host name of the end point. +/// +public interface IHostNameFeature +{ + /// + /// Gets the host name of the end point. + /// + public string HostName { get; } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4b5783cd0d5..502ef7d04c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -13,13 +13,15 @@ internal sealed partial class DnsServiceEndPointResolver( string hostName, IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + string IHostNameFeature.HostName => hostName; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -28,7 +30,9 @@ protected override async Task ResolveAsyncCore() var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(address, 0))); + var endPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + endPoint.Features.Set(this); + endPoints.Add(endPoint); } if (endPoints.Count == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 63f649531e8..260cb99242a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.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.Diagnostics; @@ -31,7 +31,7 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin /// The service name. /// The logger. /// The time provider. - public DnsServiceEndPointResolverBase( + protected DnsServiceEndPointResolverBase( string serviceName, ILogger logger, TimeProvider timeProvider) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index cb6043f94ac..5bf3065fe42 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -13,16 +13,19 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsSrvServiceEndPointResolver( string serviceName, string srvQuery, + string hostName, IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider) + TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + string IHostNameFeature.HostName => hostName; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -53,11 +56,11 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(ServiceEndPoint.Create(new IPEndPoint(addressRecord.Address, record.Port))); + endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(ServiceEndPoint.Create(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } @@ -78,5 +81,12 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) }; return new InvalidOperationException(msg); } + + ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + return serviceEndPoint; + } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index 09d9ddbc556..e5a7e23710e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -58,7 +58,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi var portName = parts.EndPointName ?? "default"; var srvQuery = $"_{portName}._tcp.{parts.Host}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, options, logger, dnsClient, timeProvider); + resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: parts.Host, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4f511cbf37b..39792f2ca89 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -1,6 +1,7 @@ // 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; using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; @@ -11,7 +12,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// A service endpoint resolver that uses configuration to resolve endpoints. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver, IHostNameFeature { private readonly string _serviceName; private readonly string? _endpointName; @@ -49,6 +50,8 @@ public ConfigurationServiceEndPointResolver( /// public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => new(ResolveInternal(endPoints)); + string IHostNameFeature.HostName => _serviceName; + private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) { // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. @@ -91,7 +94,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); } - endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } } @@ -105,7 +108,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); } - endPoints.EndPoints.Add(ServiceEndPoint.Create(endPoint)); + endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } @@ -117,6 +120,13 @@ static bool SchemesMatch(string? scheme, ServiceNameParts parts) => || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); } + private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + return serviceEndPoint; + } + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) { var configPath = new StringBuilder(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 1edf4cf4898..50a86722feb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -24,6 +24,7 @@ protected override async Task SendAsync(HttpRequestMessage { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index b8e0368c5e3..41331ef5215 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -44,6 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); request.RequestUri = GetUriWithEndPoint(originalUri, result); + request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 558d7260f52..847c9268bf5 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -210,6 +210,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 9090), initialResult.EndPoints[1].EndPoint); } + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index 5b0ec89d7a1..b19195d688e 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -43,6 +43,13 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() Assert.Equal(ResolutionStatus.Success, initialResult.Status); var ep = Assert.Single(initialResult.EndPoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } @@ -78,6 +85,13 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() 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.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } @@ -115,6 +129,13 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.Equal(2, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].EndPoint); + + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } } From 81b2daa265fd68e04bc66ea2c73a5b36c50bb7e2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:29:37 -0700 Subject: [PATCH 06/77] Fix race in ServiceEndPointResolver (#511) --- .../ServiceEndPointResolver.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index a1ee606b688..d9f847cd012 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -202,6 +202,20 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task + // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // that will have more overhead in the common case. + if (newCacheState is CacheStatus.Valid) + { + Interlocked.Exchange(ref _cachedEndPoints, null); + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + lock (_lock) { if (newCacheState is CacheStatus.Valid) @@ -213,11 +227,6 @@ private async Task RefreshAsyncInternal() _cacheState = newCacheState; } - if (OnEndPointsUpdated is { } callback) - { - callback(new(newEndPoints, status)); - } - if (error is not null) { _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); From aad81ea6db7a0e368442953de9defe1672888405 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 26 Oct 2023 20:09:18 -0700 Subject: [PATCH 07/77] Fix one place where service discovery isn't AOT compatible (#540) --- ...Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 1 + .../Configuration/ConfigurationServiceEndPointResolver.cs | 5 +++-- .../Microsoft.Extensions.ServiceDiscovery.csproj | 3 ++- 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index f89f1ba02ac..5073885c198 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -3,6 +3,7 @@ $(NetCurrent) true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 05b59e6dee6..519c340fe7b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -3,6 +3,7 @@ $(NetCurrent) true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index eaa6cebf61c..30fed3082d2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -5,6 +5,7 @@ enable enable true + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 39792f2ca89..5bf8d750d37 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -76,9 +76,10 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Read the endpoint from the configuration. // First check if there is a collection of sections - if (section.GetChildren().Any()) + var children = section.GetChildren(); + if (children.Any()) { - var values = section.Get>(); + var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { // Use schemes if any of the URIs have a scheme set. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 916145964f2..6bad9c5c20b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,8 +1,9 @@ - + $(NetCurrent) true + true From d2b078182c0ad96eb3a7c93c0467994f824bd176 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 27 Oct 2023 10:15:44 -0700 Subject: [PATCH 08/77] Service Discovery: make host name propagation opt-in (#548) * Service Discovery: make host name propagation opt-in * Review feedback --- .../DnsServiceEndPointResolver.cs | 10 ++++-- .../DnsServiceEndPointResolverOptions.cs | 7 ++++ .../DnsSrvServiceEndPointResolver.cs | 6 +++- .../DnsSrvServiceEndPointResolverOptions.cs | 7 ++++ .../ConfigurationServiceEndPointResolver.cs | 6 +++- ...igurationServiceEndPointResolverOptions.cs | 5 +++ .../HostingExtensions.cs | 10 +++--- .../DnsSrvServiceEndPointResolverTests.cs | 32 ++++++++++++++----- ...nfigurationServiceEndPointResolverTests.cs | 8 ++--- 9 files changed, 69 insertions(+), 22 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 502ef7d04c4..1d701fdd573 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -30,9 +30,13 @@ protected override async Task ResolveAsyncCore() var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var endPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - endPoint.Features.Set(this); - endPoints.Add(endPoint); + var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + + endPoints.Add(serviceEndPoint); } if (endPoints.Count == 0) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index fee7bf2a245..37879b5f3e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs @@ -1,6 +1,8 @@ // 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; /// @@ -27,4 +29,9 @@ public class DnsServiceEndPointResolverOptions /// Gets or sets the retry period growth factor. /// public double RetryBackOffFactor { get; set; } = 2; + + /// + /// 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; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 5bf3065fe42..01ae7de5315 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -85,7 +85,11 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + return serviceEndPoint; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 83ccd9afdbe..5bac96c6c0a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs @@ -1,6 +1,8 @@ // 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; /// @@ -35,4 +37,9 @@ public class DnsSrvServiceEndPointResolverOptions /// If not specified, the provider will attempt to infer the namespace. /// public string? QuerySuffix { get; set; } + + /// + /// 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; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 5bf8d750d37..4060ad5ba9e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -124,7 +124,11 @@ static bool SchemesMatch(string? scheme, ServiceNameParts parts) => private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + return serviceEndPoint; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs index a20d12d771c..5e67a885ca9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -12,4 +12,9 @@ public 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; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 53e4c1f5bda..f938e57e629 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -4,7 +4,6 @@ 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.Internal; @@ -60,12 +59,15 @@ 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 = null) { services.AddServiceDiscoveryCore(); services.AddSingleton(); - var options = services.AddOptions(); - configureOptions?.Invoke(options); + if (configureOptions is not null) + { + services.Configure(configureOptions); + } + return services; } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 847c9268bf5..7531eec0a48 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -120,6 +120,12 @@ public async Task ResolveServiceEndPoint_Dns() 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 => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); } } @@ -170,7 +176,11 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndPointResolver(options => + { + options.QuerySuffix = ".ns"; + options.ApplyHostNameMetadata = _ => true; + }) .AddConfigurationServiceEndPointResolver(); } else @@ -202,6 +212,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo 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 => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); } else { @@ -209,14 +226,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo 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.All(initialResult.EndPoints, ep => - { - var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); - }); + Assert.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); + } } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index b19195d688e..e695362dc5d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -47,8 +47,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() Assert.All(initialResult.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); + Assert.Null(hostNameFeature); }); } } @@ -68,7 +67,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); ServiceEndPointResolver resolver; @@ -133,8 +132,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.All(initialResult.EndPoints, ep => { var hostNameFeature = ep.Features.Get(); - Assert.NotNull(hostNameFeature); - Assert.Equal("basket", hostNameFeature.HostName); + Assert.Null(hostNameFeature); }); } } From a7b6f7bae06a9c78e299feebbe8835d4b3f0f746 Mon Sep 17 00:00:00 2001 From: David Pine Date: Thu, 2 Nov 2023 11:32:44 -0500 Subject: [PATCH 09/77] Last round of triple slash, I believe. (#661) * Last round of triple slash, I believe. * Revert Aspire.sln change * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs --------- Co-authored-by: David Fowler --- .../Features/IEndPointHealthFeature.cs | 10 ++++++++-- .../Features/IEndPointLoadFeature.cs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs index 50276e05ecb..63dc3e11a3a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -3,10 +3,16 @@ 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. + /// + /// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs index d58f23c7775..2610f135945 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -3,9 +3,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that provides information about the current load of an endpoint. +/// public interface IEndPointLoadFeature { - // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + /// + /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). + /// public double CurrentLoad { get; } } From 92a239c128bbf1ddd4711f15d8e27f004b21aa08 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 2 Nov 2023 10:31:31 -0700 Subject: [PATCH 10/77] Last round of triple slash, I believe. (#661) (#663) * Last round of triple slash, I believe. * Revert Aspire.sln change * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs * Update src/Aspire.Hosting.Azure.Provisioning/UserSecretsPathHelper.cs --------- Co-authored-by: David Pine --- .../Features/IEndPointHealthFeature.cs | 10 ++++++++-- .../Features/IEndPointLoadFeature.cs | 7 ++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs index 50276e05ecb..63dc3e11a3a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs @@ -3,10 +3,16 @@ 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. + /// + /// 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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs index d58f23c7775..2610f135945 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs @@ -3,9 +3,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; +/// +/// Represents a feature that provides information about the current load of an endpoint. +/// public interface IEndPointLoadFeature { - // CurrentLoad is some comparable measure of load (queue length, concurrent requests, etc) + /// + /// Gets a comparable measure of the current load of the endpoint (e.g. queue length, concurrent requests, etc). + /// public double CurrentLoad { get; } } From 7fbb032c84508c87490aa1c9f07e5596eaad30f7 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 2 Nov 2023 15:35:59 -0700 Subject: [PATCH 11/77] Add additional debug logs to Service Discovery (#672) * Add additional debug logs to Service Discovery * Log message tweak * Use display name instead of GetType().Name for resolver names. Remove nameof --- .../IServiceEndPointResolver.cs | 5 ++ .../Internal/ServiceEndPointImpl.cs | 2 +- .../DnsServiceEndPointResolver.cs | 4 + .../DnsServiceEndPointResolverBase.Log.cs | 27 +----- .../DnsServiceEndPointResolverBase.cs | 14 +--- .../DnsSrvServiceEndPointResolver.cs | 3 + ...onfigurationServiceEndPointResolver.Log.cs | 71 ++++++++++++++++ .../ConfigurationServiceEndPointResolver.cs | 84 ++++++++++++++----- ...gurationServiceEndPointResolverProvider.cs | 8 +- .../HostingExtensions.cs | 2 +- .../Internal/ServiceNameParts.cs | 17 +++- .../PassThroughServiceEndPointResolver.Log.cs | 15 ++++ .../PassThroughServiceEndPointResolver.cs | 32 +++++++ ...ThroughServiceEndPointResolverProvider.cs} | 26 ++---- .../ServiceEndPointResolver.Log.cs | 43 ++++++++++ .../ServiceEndPointResolver.cs | 10 ++- .../ServiceEndPointResolverFactory.Log.cs | 23 +++++ .../ServiceEndPointResolverFactory.cs | 15 ++-- ...PassThroughServiceEndPointResolverTests.cs | 2 +- .../ServiceEndPointResolverTests.cs | 6 +- 20 files changed, 314 insertions(+), 95 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Internal/PassThroughServiceEndPointResolver.cs => PassThrough/PassThroughServiceEndPointResolverProvider.cs} (51%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index c228847c568..3cbb9b6c491 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,6 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { + /// + /// Gets the diagnostic display name for this resolver. + /// + string DisplayName { get; } + /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index 0b54f5a19d0..b73635ecd57 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -20,5 +20,5 @@ public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = nul public override EndPoint EndPoint => _endPoint; public override IFeatureCollection Features => _features; - public override string? ToString() => _endPoint.ToString(); + public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 1d701fdd573..b0d30530c72 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -22,6 +22,9 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; + /// + public override string DisplayName => "DNS"; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -31,6 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 81668041ae1..c3384d20e19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.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.Dns; @@ -16,31 +15,13 @@ internal static partial class Log [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); - public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) - { - if (logger.IsEnabled(LogLevel.Trace)) - { - DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); - } - else if (logger.IsEnabled(LogLevel.Debug)) - { - DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); - } - } + [LoggerMessage(3, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] - public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - - [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] - public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - - [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] - public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); - - [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + [LoggerMessage(4, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); - [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + [LoggerMessage(5, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 260cb99242a..238ace927fa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.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 System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,6 +44,8 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } + public abstract string DisplayName { get; } + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } @@ -61,6 +62,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS // 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; } @@ -161,16 +163,6 @@ private void SetResult(List? endPoints, Exception? exception, T _lastEndPointCollection = endPoints; } - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, ServiceName); - } - TimeSpan GetRefreshPeriod() { if (_lastStatus.StatusCode is ResolutionStatusCode.Success) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 01ae7de5315..ce8ae159764 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -24,6 +24,8 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + public override string DisplayName => "DNS SRV"; + string IHostNameFeature.HostName => hostName; protected override async Task ResolveAsyncCore() @@ -85,6 +87,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..b7e43172740 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -0,0 +1,71 @@ +// 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; + +internal sealed partial class ConfigurationServiceEndPointResolver +{ + 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 endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + + [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] + internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + + [LoggerMessage(6, 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) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + StringBuilder endpointValues = new(); + for (var i = 0; i < parsedValues.Count; i++) + { + if (endpointValues.Length > 0) + { + endpointValues.Append(", "); + } + + endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + } + + var configuredEndPoints = endpointValues.ToString(); + ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4060ad5ba9e..fd382487551 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -17,6 +18,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd private readonly string _serviceName; private readonly string? _endpointName; private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly IOptions _options; /// @@ -24,10 +26,12 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// The service name. /// The configuration. + /// The logger. /// The options. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, + ILogger logger, IOptions options) { if (ServiceNameParts.TryParse(serviceName, out var parts)) @@ -37,13 +41,17 @@ public ConfigurationServiceEndPointResolver( } else { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); } _configuration = configuration; + _logger = logger; _options = options; } + /// + public string DisplayName => "Configuration"; + /// public ValueTask DisposeAsync() => default; @@ -57,6 +65,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Only add endpoints 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; } @@ -69,9 +78,11 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Get the corresponding config section. var section = root.GetSection(_serviceName); + var configPath = GetConfigurationPath(baseSectionName); + Log.UsingConfigurationPath(_logger, configPath, _serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, baseSectionName); + return CreateNotFoundResponse(endPoints, configPath); } // Read the endpoint from the configuration. @@ -82,17 +93,21 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { - // Use schemes if any of the URIs have a scheme set. - var uris = ParseServiceNameParts(values); - var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - foreach (var uri in uris) + // Use endpoint names if any of the values have an endpoint name set. + var parsedValues = ParseServiceNameParts(values, configPath); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); + + var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + + foreach (var uri in parsedValues) { - // If either schemes are not in-use or the scheme matches, create an endpoint for this value - if (!useSchemes || SchemesMatch(_endpointName, uri)) + // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. + if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) { if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); @@ -100,30 +115,42 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) { - if (SchemesMatch(_endpointName, uri)) + if (EndPointNamesMatch(_endpointName, parsed)) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + } + + if (_logger.IsEnabled(LogLevel.Debug)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } + if (endPoints.EndPoints.Count == 0) + { + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + } + endPoints.AddChangeToken(section.GetReloadToken()); return ResolutionStatus.Success; - static bool SchemesMatch(string? scheme, ServiceNameParts parts) => - (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) - || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => + string.IsNullOrEmpty(parts.EndPointName) + || string.IsNullOrEmpty(endPointName) + || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); } private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); @@ -132,7 +159,14 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); + } + + private string GetConfigurationPath(string? baseSectionName) { var configPath = new StringBuilder(); if (baseSectionName is { Length: > 0 }) @@ -141,21 +175,29 @@ private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource } configPath.Append(_serviceName); - endPoints.AddChangeToken(_configuration.GetReloadToken()); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + return configPath.ToString(); } - private static List ParseServiceNameParts(List input) + private List ParseServiceNameParts(List input, string configPath) { var results = new List(input.Count); for (var i = 0; i < input.Count; ++i) { if (ServiceNameParts.TryParse(input[i], out var value)) { - results.Add(value); + if (!results.Contains(value)) + { + results.Add(value); + } + } + else + { + throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); } } return results; } + + public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 5c35b6161fd..affe3a655ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -12,17 +13,20 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// The configuration. /// The options. +/// The logger factory. public class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, - IOptions options) : IServiceEndPointResolverProvider + IOptions options, + ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider { private readonly IConfiguration _configuration = configuration; private readonly IOptions _options = options; + private readonly ILogger _logger = loggerFactory.CreateLogger(); /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index f938e57e629..94f806a4016 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 8b2a03acd31..0d310190a33 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal readonly struct ServiceNameParts +internal readonly struct ServiceNameParts : IEquatable { public ServiceNameParts(string host, string? endPointName, int port) : this() { @@ -21,6 +21,8 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public int Port { get; init; } + public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; + public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) { if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) @@ -93,5 +95,18 @@ public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out serviceEndPoint = null; return false; } + + 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/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..570eb5e4e47 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs @@ -0,0 +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 Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +internal sealed partial class PassThroughServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + internal static partial void UsingPassThrough(ILogger logger, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..4ee6b0dd32d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,32 @@ +// 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; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +/// +/// Service endpoint resolver which passes through the provided value. +/// +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +{ + public string DisplayName => "Pass-through"; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + 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); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs similarity index 51% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 15e566c5887..24028f24ee5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -3,14 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Internal; +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) @@ -21,25 +23,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(endPoint); + resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } - - private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver - { - private readonly EndPoint _endPoint = endPoint; - - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) - { - if (endPoints.EndPoints.Count != 0) - { - return new(ResolutionStatus.None); - } - - endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); - return new(ResolutionStatus.Success); - } - - public ValueTask DisposeAsync() => default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..274d471030d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -0,0 +1,43 @@ +// 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.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public sealed partial class ServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] + public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + + [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] + public static partial void ResolutionPending(ILogger logger, string serviceName); + + [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) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + } + + static string GetEndPointString(ServiceEndPoint ep) + { + if (ep.Features.Get() is { } resolver) + { + return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + } + + return ep.GetEndPointString(); + } + } + + [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index d9f847cd012..b8356d56af5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves endpoints for a specified service. /// -public sealed class ServiceEndPointResolver( +public sealed partial class ServiceEndPointResolver( IServiceEndPointResolver[] resolvers, ILogger logger, string serviceName, @@ -135,6 +135,7 @@ private async Task RefreshAsyncInternal() { 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); @@ -148,6 +149,7 @@ private async Task RefreshAsyncInternal() 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; } @@ -229,12 +231,12 @@ private async Task RefreshAsyncInternal() if (error is not null) { - _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + else if (newEndPoints is not null) { - _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs new file mode 100644 index 00000000000..23d4b03cd67 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -0,0 +1,23 @@ +// 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.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public partial class ServiceEndPointResolverFactory +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.DisplayName))); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 94696ceb413..ce020178406 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -4,14 +4,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public class ServiceEndPointResolverFactory( +public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, ILogger resolverLogger, IOptions options, @@ -20,7 +20,7 @@ public class ServiceEndPointResolverFactory( private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _resolverLogger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; @@ -36,19 +36,20 @@ public ServiceEndPointResolver CreateResolver(string serviceName) { if (factory.TryCreateResolver(serviceName, out var resolver)) { - resolvers ??= new(); + resolvers ??= []; resolvers.Add(resolver); } } if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); + throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); } + Log.CreatingResolver(_logger, serviceName, resolvers); return new ServiceEndPointResolver( - resolvers: resolvers.ToArray(), - logger: _resolverLogger, + resolvers: [.. resolvers], + logger: _logger, serviceName: serviceName, timeProvider: _timeProvider, options: _options); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 3ede6371deb..9d3e6ac17e7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -7,7 +7,7 @@ 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; namespace Microsoft.Extensions.ServiceDiscovery.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index aa91a41f887..c435f965fe6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -26,7 +26,7 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] @@ -61,7 +61,7 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() 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 has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider @@ -76,6 +76,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { + public string DisplayName => "Fake"; + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 2371e8b85940c086027af0b997ade21d0ae8f42f Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 2 Nov 2023 17:03:18 -0700 Subject: [PATCH 12/77] Add additional debug logs to Service Discovery (#672) (#678) * Add additional debug logs to Service Discovery * Log message tweak * Use display name instead of GetType().Name for resolver names. Remove nameof --- .../IServiceEndPointResolver.cs | 5 ++ .../Internal/ServiceEndPointImpl.cs | 2 +- .../DnsServiceEndPointResolver.cs | 4 + .../DnsServiceEndPointResolverBase.Log.cs | 27 +----- .../DnsServiceEndPointResolverBase.cs | 14 +--- .../DnsSrvServiceEndPointResolver.cs | 3 + ...onfigurationServiceEndPointResolver.Log.cs | 71 ++++++++++++++++ .../ConfigurationServiceEndPointResolver.cs | 84 ++++++++++++++----- ...gurationServiceEndPointResolverProvider.cs | 8 +- .../HostingExtensions.cs | 2 +- .../Internal/ServiceNameParts.cs | 17 +++- .../PassThroughServiceEndPointResolver.Log.cs | 15 ++++ .../PassThroughServiceEndPointResolver.cs | 32 +++++++ ...ThroughServiceEndPointResolverProvider.cs} | 26 ++---- .../ServiceEndPointResolver.Log.cs | 43 ++++++++++ .../ServiceEndPointResolver.cs | 10 ++- .../ServiceEndPointResolverFactory.Log.cs | 23 +++++ .../ServiceEndPointResolverFactory.cs | 15 ++-- ...PassThroughServiceEndPointResolverTests.cs | 2 +- .../ServiceEndPointResolverTests.cs | 6 +- 20 files changed, 314 insertions(+), 95 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Internal/PassThroughServiceEndPointResolver.cs => PassThrough/PassThroughServiceEndPointResolverProvider.cs} (51%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index c228847c568..3cbb9b6c491 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,6 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { + /// + /// Gets the diagnostic display name for this resolver. + /// + string DisplayName { get; } + /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index 0b54f5a19d0..b73635ecd57 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs @@ -20,5 +20,5 @@ public ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = nul public override EndPoint EndPoint => _endPoint; public override IFeatureCollection Features => _features; - public override string? ToString() => _endPoint.ToString(); + public override string? ToString() => GetEndPointString(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 1d701fdd573..b0d30530c72 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -22,6 +22,9 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; + /// + public override string DisplayName => "DNS"; + protected override async Task ResolveAsyncCore() { var endPoints = new List(); @@ -31,6 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index 81668041ae1..c3384d20e19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.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.Dns; @@ -16,31 +15,13 @@ internal static partial class Log [LoggerMessage(2, LogLevel.Trace, "Resolving endpoints for service '{ServiceName}' using host lookup for name '{RecordName}'.", EventName = "AddressQuery")] public static partial void AddressQuery(ILogger logger, string serviceName, string recordName); - public static void DiscoveredEndPoints(ILogger logger, List endPoints, string serviceName, TimeSpan ttl) - { - if (logger.IsEnabled(LogLevel.Trace)) - { - DiscoveredEndPointsCoreTrace(logger, endPoints.Count, serviceName, ttl, string.Join(", ", endPoints.Select(static ep => ep.GetEndPointString()))); - } - else if (logger.IsEnabled(LogLevel.Debug)) - { - DiscoveredEndPointsCoreDebug(logger, endPoints.Count, serviceName, ttl); - } - } + [LoggerMessage(3, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + public static partial void SkippedResolution(ILogger logger, string serviceName, string reason); - [LoggerMessage(3, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}.", EventName = "DiscoveredEndPoints")] - public static partial void DiscoveredEndPointsCoreDebug(ILogger logger, int count, string serviceName, TimeSpan ttl); - - [LoggerMessage(4, LogLevel.Debug, "Discovered {Count} endpoints for service '{ServiceName}'. Will refresh in {Ttl}. EndPoints: {EndPoints}", EventName = "DiscoveredEndPointsDetailed")] - public static partial void DiscoveredEndPointsCoreTrace(ILogger logger, int count, string serviceName, TimeSpan ttl, string endPoints); - - [LoggerMessage(5, LogLevel.Warning, "Endpoints resolution failed for service '{ServiceName}'.", EventName = "ResolutionFailed")] - public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); - - [LoggerMessage(6, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] + [LoggerMessage(4, LogLevel.Debug, "Service name '{ServiceName}' is not a valid URI or DNS name.", EventName = "ServiceNameIsNotUriOrDnsName")] public static partial void ServiceNameIsNotUriOrDnsName(ILogger logger, string serviceName); - [LoggerMessage(7, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] + [LoggerMessage(5, LogLevel.Debug, "DNS SRV query cannot be constructed for service name '{ServiceName}' because no DNS namespace was configured or detected.", EventName = "NoDnsSuffixFound")] public static partial void NoDnsSuffixFound(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 260cb99242a..238ace927fa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.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 System.Diagnostics; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,6 +44,8 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } + public abstract string DisplayName { get; } + private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } @@ -61,6 +62,7 @@ public async ValueTask ResolveAsync(ServiceEndPointCollectionS // 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; } @@ -161,16 +163,6 @@ private void SetResult(List? endPoints, Exception? exception, T _lastEndPointCollection = endPoints; } - if (exception is null) - { - Debug.Assert(endPoints is not null); - Log.DiscoveredEndPoints(_logger, endPoints, ServiceName, validityPeriod); - } - else - { - Log.ResolutionFailed(_logger, exception, ServiceName); - } - TimeSpan GetRefreshPeriod() { if (_lastStatus.StatusCode is ResolutionStatusCode.Success) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 01ae7de5315..ce8ae159764 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -24,6 +24,8 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; + public override string DisplayName => "DNS SRV"; + string IHostNameFeature.HostName => hostName; protected override async Task ResolveAsyncCore() @@ -85,6 +87,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..b7e43172740 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -0,0 +1,71 @@ +// 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; + +internal sealed partial class ConfigurationServiceEndPointResolver +{ + 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 endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] + public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + + [LoggerMessage(5, LogLevel.Debug, "No endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] + internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + + [LoggerMessage(6, 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) + { + if (!logger.IsEnabled(LogLevel.Debug)) + { + return; + } + + StringBuilder endpointValues = new(); + for (var i = 0; i < parsedValues.Count; i++) + { + if (endpointValues.Length > 0) + { + endpointValues.Append(", "); + } + + endpointValues.Append(CultureInfo.InvariantCulture, $"({parsedValues[i]})"); + } + + var configuredEndPoints = endpointValues.ToString(); + ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 4060ad5ba9e..fd382487551 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -17,6 +18,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd private readonly string _serviceName; private readonly string? _endpointName; private readonly IConfiguration _configuration; + private readonly ILogger _logger; private readonly IOptions _options; /// @@ -24,10 +26,12 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// /// The service name. /// The configuration. + /// The logger. /// The options. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, + ILogger logger, IOptions options) { if (ServiceNameParts.TryParse(serviceName, out var parts)) @@ -37,13 +41,17 @@ public ConfigurationServiceEndPointResolver( } else { - throw new InvalidOperationException($"Service name '{serviceName}' is not valid"); + throw new InvalidOperationException($"Service name '{serviceName}' is not valid."); } _configuration = configuration; + _logger = logger; _options = options; } + /// + public string DisplayName => "Configuration"; + /// public ValueTask DisposeAsync() => default; @@ -57,6 +65,7 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Only add endpoints 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; } @@ -69,9 +78,11 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin // Get the corresponding config section. var section = root.GetSection(_serviceName); + var configPath = GetConfigurationPath(baseSectionName); + Log.UsingConfigurationPath(_logger, configPath, _serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, baseSectionName); + return CreateNotFoundResponse(endPoints, configPath); } // Read the endpoint from the configuration. @@ -82,17 +93,21 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); if (values is { Count: > 0 }) { - // Use schemes if any of the URIs have a scheme set. - var uris = ParseServiceNameParts(values); - var useSchemes = !uris.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - foreach (var uri in uris) + // Use endpoint names if any of the values have an endpoint name set. + var parsedValues = ParseServiceNameParts(values, configPath); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); + + var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); + Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + + foreach (var uri in parsedValues) { - // If either schemes are not in-use or the scheme matches, create an endpoint for this value - if (!useSchemes || SchemesMatch(_endpointName, uri)) + // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. + if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) { if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); @@ -100,30 +115,42 @@ private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoin } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var uri)) + else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) { - if (SchemesMatch(_endpointName, uri)) + if (EndPointNamesMatch(_endpointName, parsed)) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) + if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + { + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + } + + if (_logger.IsEnabled(LogLevel.Debug)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The configuration section for service endpoint {_serviceName} is invalid.")); + Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); } endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } + if (endPoints.EndPoints.Count == 0) + { + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + } + endPoints.AddChangeToken(section.GetReloadToken()); return ResolutionStatus.Success; - static bool SchemesMatch(string? scheme, ServiceNameParts parts) => - (string.IsNullOrEmpty(parts.EndPointName) || string.IsNullOrEmpty(scheme)) - || MemoryExtensions.Equals(parts.EndPointName, scheme, StringComparison.OrdinalIgnoreCase); + static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => + string.IsNullOrEmpty(parts.EndPointName) + || string.IsNullOrEmpty(endPointName) + || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); } private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); @@ -132,7 +159,14 @@ private ServiceEndPoint CreateEndPoint(EndPoint endPoint) return serviceEndPoint; } - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string? baseSectionName) + private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) + { + endPoints.AddChangeToken(_configuration.GetReloadToken()); + Log.ConfigurationNotFound(_logger, _serviceName, configPath); + return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); + } + + private string GetConfigurationPath(string? baseSectionName) { var configPath = new StringBuilder(); if (baseSectionName is { Length: > 0 }) @@ -141,21 +175,29 @@ private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource } configPath.Append(_serviceName); - endPoints.AddChangeToken(_configuration.GetReloadToken()); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path \"{configPath}\" was found"); + return configPath.ToString(); } - private static List ParseServiceNameParts(List input) + private List ParseServiceNameParts(List input, string configPath) { var results = new List(input.Count); for (var i = 0; i < input.Count; ++i) { if (ServiceNameParts.TryParse(input[i], out var value)) { - results.Add(value); + if (!results.Contains(value)) + { + results.Add(value); + } + } + else + { + throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); } } return results; } + + public override string ToString() => "Configuration"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 5c35b6161fd..affe3a655ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -12,17 +13,20 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// The configuration. /// The options. +/// The logger factory. public class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, - IOptions options) : IServiceEndPointResolverProvider + IOptions options, + ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider { private readonly IConfiguration _configuration = configuration; private readonly IOptions _options = options; + private readonly ILogger _logger = loggerFactory.CreateLogger(); /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index f938e57e629..94f806a4016 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 8b2a03acd31..0d310190a33 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal readonly struct ServiceNameParts +internal readonly struct ServiceNameParts : IEquatable { public ServiceNameParts(string host, string? endPointName, int port) : this() { @@ -21,6 +21,8 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() public int Port { get; init; } + public override string? ToString() => EndPointName is not null ? $"EndPointName: {EndPointName}, Host: {Host}, Port: {Port}" : $"Host: {Host}, Port: {Port}"; + public static bool TryParse(string serviceName, [NotNullWhen(true)] out ServiceNameParts parts) { if (serviceName.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) @@ -93,5 +95,18 @@ public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out serviceEndPoint = null; return false; } + + 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/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..570eb5e4e47 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs @@ -0,0 +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 Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +internal sealed partial class PassThroughServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + internal static partial void UsingPassThrough(ILogger logger, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs new file mode 100644 index 00000000000..4ee6b0dd32d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -0,0 +1,32 @@ +// 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; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; + +/// +/// Service endpoint resolver which passes through the provided value. +/// +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +{ + public string DisplayName => "Pass-through"; + + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) + { + 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); + return new(ResolutionStatus.Success); + } + + public ValueTask DisposeAsync() => default; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs similarity index 51% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 15e566c5887..24028f24ee5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -3,14 +3,16 @@ using System.Diagnostics.CodeAnalysis; using System.Net; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Internal; -namespace Microsoft.Extensions.ServiceDiscovery.Internal; +namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver provider which passes through the provided value. /// -internal sealed class PassThroughServiceEndPointResolverProvider : IServiceEndPointResolverProvider +internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) @@ -21,25 +23,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(endPoint); + resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } - - private sealed class PassThroughServiceEndPointResolver(EndPoint endPoint) : IServiceEndPointResolver - { - private readonly EndPoint _endPoint = endPoint; - - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) - { - if (endPoints.EndPoints.Count != 0) - { - return new(ResolutionStatus.None); - } - - endPoints.EndPoints.Add(ServiceEndPoint.Create(_endPoint)); - return new(ResolutionStatus.Success); - } - - public ValueTask DisposeAsync() => default; - } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs new file mode 100644 index 00000000000..274d471030d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -0,0 +1,43 @@ +// 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.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public sealed partial class ServiceEndPointResolver +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] + public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + + [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] + public static partial void ResolutionPending(ILogger logger, string serviceName); + + [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) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ResolutionSucceededCore(logger, endPoints.Count, serviceName, string.Join(", ", endPoints.Select(GetEndPointString))); + } + + static string GetEndPointString(ServiceEndPoint ep) + { + if (ep.Features.Get() is { } resolver) + { + return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + } + + return ep.GetEndPointString(); + } + } + + [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] + public static partial void ResolutionFailed(ILogger logger, Exception exception, string serviceName); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index d9f847cd012..b8356d56af5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves endpoints for a specified service. /// -public sealed class ServiceEndPointResolver( +public sealed partial class ServiceEndPointResolver( IServiceEndPointResolver[] resolvers, ILogger logger, string serviceName, @@ -135,6 +135,7 @@ private async Task RefreshAsyncInternal() { 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); @@ -148,6 +149,7 @@ private async Task RefreshAsyncInternal() 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; } @@ -229,12 +231,12 @@ private async Task RefreshAsyncInternal() if (error is not null) { - _logger.LogError(error, "Error resolving service {ServiceName}", ServiceName); + Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (_logger.IsEnabled(LogLevel.Debug) && newEndPoints is not null) + else if (newEndPoints is not null) { - _logger.LogDebug("Resolved service {ServiceName} to {EndPoints}", ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs new file mode 100644 index 00000000000..23d4b03cd67 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -0,0 +1,23 @@ +// 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.Logging; +using Microsoft.Extensions.ServiceDiscovery.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery; + +public partial class ServiceEndPointResolverFactory +{ + private sealed partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.DisplayName))); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index 94696ceb413..ce020178406 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -4,14 +4,14 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.ServiceDiscovery; /// /// Creates instances. /// -public class ServiceEndPointResolverFactory( +public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, ILogger resolverLogger, IOptions options, @@ -20,7 +20,7 @@ public class ServiceEndPointResolverFactory( private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _resolverLogger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; @@ -36,19 +36,20 @@ public ServiceEndPointResolver CreateResolver(string serviceName) { if (factory.TryCreateResolver(serviceName, out var resolver)) { - resolvers ??= new(); + resolvers ??= []; resolvers.Add(resolver); } } if (resolvers is not { Count: > 0 }) { - throw new InvalidOperationException("No resolver which supports the provided service name has been configured."); + throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); } + Log.CreatingResolver(_logger, serviceName, resolvers); return new ServiceEndPointResolver( - resolvers: resolvers.ToArray(), - logger: _resolverLogger, + resolvers: [.. resolvers], + logger: _logger, serviceName: serviceName, timeProvider: _timeProvider, options: _options); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 3ede6371deb..9d3e6ac17e7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -7,7 +7,7 @@ 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; namespace Microsoft.Extensions.ServiceDiscovery.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index aa91a41f887..c435f965fe6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -26,7 +26,7 @@ public void ResolveServiceEndPoint_NoResolversConfigured_Throws() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); var exception = Assert.Throws(() => resolverFactory.CreateResolver("https://basket")); - Assert.Equal("No resolver which supports the provided service name has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] @@ -61,7 +61,7 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() 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 has been configured.", exception.Message); + Assert.Equal("No resolver which supports the provided service name, 'http://foo', has been configured.", exception.Message); } private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointResolverProvider @@ -76,6 +76,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { + public string DisplayName => "Fake"; + public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 655a8a2a550dfea9c9926c2449a6566628ef34b3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 6 Nov 2023 15:35:15 +0800 Subject: [PATCH 13/77] Add package descriptions and icons. (#701) * Add package descriptions and icons. --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 5073885c198..295279cdefd 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 519c340fe7b..60ad214e883 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS SRV records. Useful for service resolution in orchestrators such as Kubernetes. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 30fed3082d2..6602d8ad88c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -6,6 +6,8 @@ enable true true + Provides extensions for service discovery for the YARP reverse proxy. + $(SharedDir)dotnet-icon.png diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6bad9c5c20b..76862965b28 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient that enable service discovery based on configuration and dns. + $(SharedDir)dotnet-icon.png From db14b2701d044949a107a443ff1335b83d6cb34a Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 7 Nov 2023 09:03:10 +0800 Subject: [PATCH 14/77] [release/8.0-preview1] Add package descriptions and icons. (#701) (#702) * Add package descriptions and icons. (#701) * Add package descriptions and icons. * Update src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj Co-authored-by: Eric Erhardt * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> * Update src/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> * Fix package icons. Use base arcade icon for ServiceDiscovery packages. Use the default Aspire icon for Aspire Hosting packages. --------- Co-authored-by: David Fowler Co-authored-by: Eric Erhardt Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 ++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 ++ 4 files changed, 8 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 5073885c198..0ec16344e2a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 519c340fe7b..a3036bf028f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 30fed3082d2..729e5f9f405 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -6,6 +6,8 @@ enable true true + Provides extensions for service discovery for the YARP reverse proxy. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6bad9c5c20b..81a21ac841a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,6 +4,8 @@ $(NetCurrent) true true + Provides extensions to HttpClient that enable service discovery based on configuration. + true From fd54a80a12aec9fd30ca670d615bc62a9df8f41b Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 7 Nov 2023 23:40:40 +0800 Subject: [PATCH 15/77] Forward port package changes from release branch. (#722) * Forward port package changes from release branch. * Remove unused dotnet-icon. --------- Co-authored-by: Eric Erhardt --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ++-- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 295279cdefd..0ec16344e2a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -5,7 +5,7 @@ true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. - $(SharedDir)dotnet-icon.png + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 60ad214e883..a3036bf028f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -4,8 +4,8 @@ $(NetCurrent) true true - Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS SRV records. Useful for service resolution in orchestrators such as Kubernetes. - $(SharedDir)dotnet-icon.png + Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 6602d8ad88c..729e5f9f405 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true true Provides extensions for service discovery for the YARP reverse proxy. - $(SharedDir)dotnet-icon.png + true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 76862965b28..81a21ac841a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -4,8 +4,8 @@ $(NetCurrent) true true - Provides extensions to HttpClient that enable service discovery based on configuration and dns. - $(SharedDir)dotnet-icon.png + Provides extensions to HttpClient that enable service discovery based on configuration. + true From 381a5ed58b139e3683e2999f178690c646c54923 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 13 Nov 2023 18:11:15 -0600 Subject: [PATCH 16/77] Fix Trim warning in ServiceDiscovery (#768) When PublishTrimmed=true in an app that uses ServiceDiscovery, we get a warning that says: _ILLink : warning IL2105: Microsoft.Extensions.ServiceDiscovery.Abstractions.ServiceEndPointCollection: Type 'ServiceEndPointCollectionDebuggerView' was not found in the caller assembly nor in the base library. Type name strings used for dynamically accessing a type should be assembly qualified._ Fix this warning by using `typeof` instead of `nameof`. --- .../ServiceEndPointCollection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs index 57919f949c3..c9540f2e7a1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs @@ -12,7 +12,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// Represents an immutable collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(nameof(ServiceEndPointCollectionDebuggerView))] +[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] public class ServiceEndPointCollection : IReadOnlyList { private readonly List? _endpoints; From 873440f97ec79433f8baa67bd9a37a5de76450c1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:02:58 -0800 Subject: [PATCH 17/77] READMEs for Service Discovery (#792) * READMEs for Service Discovery * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery.Dns/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine * Update src/Microsoft.Extensions.ServiceDiscovery/README.md Co-authored-by: David Pine --------- Co-authored-by: David Pine --- .../README.md | 7 + .../README.md | 65 +++++ .../README.md | 42 +++ .../README.md | 276 ++++++++++++++++++ 4 files changed, 390 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md new file mode 100644 index 00000000000..c5cf6b9bc78 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -0,0 +1,7 @@ +# Microsoft.Extensions.ServiceDiscovery.Abstractions + +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md new file mode 100644 index 00000000000..d3fbe2a75e5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -0,0 +1,65 @@ +# Microsoft.Extensions.ServiceDiscovery.Dns + +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: + +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. + +- _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). + +## Resolving service endpoints with DNS + +The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. + +To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsServiceEndPointResolver(); +``` + +## Resolving service endpoints in Kubernetes with DNS SRV + +When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". + +```yml +apiVersion: v1 +kind: Service +metadata: + name: basket +spec: + selector: + name: basket-service + clusterIP: None + ports: + - name: default + port: 8080 + - name: dashboard + port: 8888 +``` + +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsSrvServiceEndPointResolver(); +``` + +The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. + +As in the previous example, add service discovery to an `HttpClient` for the basket service: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +``` + +Similarly, the "dashboard" endpoint can be targeted as follows: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md new file mode 100644 index 00000000000..a7175f0382c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/README.md @@ -0,0 +1,42 @@ +# Microsoft.Extensions.ServiceDiscovery.Yarp + +The `Microsoft.Extensions.ServiceDiscovery.Yarp` library adds support for resolving endpoints for YARP clusters, by implementing a [YARP destination resolver](https://github.com/microsoft/reverse-proxy/blob/main/docs/docfx/articles/destination-resolvers.md). + +## Usage + +### Resolving YARP cluster destinations using Service Discovery + +The `IReverseProxyBuilder.AddServiceDiscoveryDestinationResolver()` extension method configures a [YARP destination resolver](https://github.com/microsoft/reverse-proxy/blob/main/docs/docfx/articles/destination-resolvers.md). To use this method, you must also configure YARP itself as described in the YARP documentation, and you must configure .NET Service Discovery via the _Microsoft.Extensions.ServiceDiscovery_ library. + +### Direct HTTP forwarding using Service Discovery Forwarding HTTP requests using `IHttpForwarder` + +YARP supports _direct forwarding_ of specific requests using the `IHttpForwarder` interface. This, too, can benefit from service discovery using the _Microsoft.Extensions.ServiceDiscovery_ library. To take advantage of service discovery when using YARP Direct Forwarding, use the `IServiceCollection.AddHttpForwarderWithServiceDiscovery` method. + +For example, consider the following .NET Aspire application: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +// Configure service discovery +builder.Services.AddServiceDiscovery(); + +// Add YARP Direct Forwarding with Service Discovery support +builder.Services.AddHttpForwarderWithServiceDiscovery(); + +// ... other configuration ... + +var app = builder.Build(); + +// ... other configuration ... + +// Map a Direct Forwarder which forwards requests to the resolved "catalogservice" endpoints +app.MapForwarder("/catalog/images/{id}", "http://catalogservice", "/api/v1/catalog/items/{id}/image"); + +app.Run(); +``` + +In the above example, the YARP Direct Forwarder will resolve the _catalogservice_ using service discovery, forwarding request sent to the `/catalog/images/{id}` endpoint to the destination path on the resolved endpoints. + +## Feedback & contributing + +https://github.com/dotnet/aspire diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md new file mode 100644 index 00000000000..7ae0e4872c8 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -0,0 +1,276 @@ +# Microsoft.Extensions.ServiceDiscovery + +The `Microsoft.Extensions.ServiceDiscovery` library is designed to simplify the integration of service discovery patterns in .NET applications. Service discovery is a key component of most distributed systems and microservices architectures. This library provides a straightforward way to resolve service names to endpoint addresses. + +In typical systems, service configuration changes over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. + +## How it works + +Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). + +Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. + +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. + +Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. + +### Change notifications + +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). + +### Extensibility using features + +Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: + +* `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). +* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. +* `IEndPointLoadFeature`: used to query estimated endpoint load. + +### Resolution order + +The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. + +## Getting Started + +### Installation + +To install the library, use the following NuGet command: + +```dotnetcli +dotnet add package Microsoft.Extensions.ServiceDiscovery +``` + +### Usage example + +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. + +```csharp +builder.Services.AddServiceDiscovery(); +``` + +Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: + +```csharp +builder.Services.AddHttpClient(c => +{ + c.BaseAddress = new("http://catalog")); +}).UseServiceDiscovery(); +``` + +Alternatively, you can add service discovery to all `HttpClient` instances by default: + +```csharp +builder.Services.ConfigureHttpClientDefaults(http => +{ + // Turn on service discovery by default + http.UseServiceDiscovery(); +}); +``` + +### Resolving service endpoints from configuration + +The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. +This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. + +Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: + +```json +{ + "Services": { + "catalog": [ + "localhost:8080", + "10.46.24.90:80", + ] + } +} +``` + +The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. +Each time the _catalog_ is resolved, one of these endpoints will be selected. + +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. + +### Configuration + +The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: + +* **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. + +* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. + +To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +builder.Services.Configure(options => +{ + options.SectionName = "MyServiceEndpoints"; + + // Configure the logic for applying host name metadata + options.ApplyHostNameMetadata = endpoint => + { + // Your custom logic here. For example: + return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + }; +}); +``` + +This example demonstrates setting a custom section name for your service endpoints and providing a custom logic for applying host name metadata based on a condition. + +## Resolving service endpoints using platform-provided service discovery + +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. + +The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. + +The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. + +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. + +In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". + +## Load-balancing with endpoint selectors + +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://catalog")); + .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); +``` + +The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: + +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` + +Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. + +## Service discovery in .NET Aspire + +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. + +Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var catalog = builder.AddProject("catalog"); +var basket = builder.AddProject("basket"); + +var frontend = builder.AddProject("frontend") + .WithReference(basket) + .WithReference(catalog); +``` + +In the above example, the _frontend_ project references the _catalog_ project and the _basket_ project. The two `WithReference` calls instruct the .NET Aspire application to pass service discovery information for the referenced projects (_catalog_, and _basket_) into the _frontend_ project. + +## Named endpoints + +Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `http://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `http://_dashboard.basket` can be used to specify this endpoint, for example: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +In the above example, two `HttpClient`s are added: one for the core basket service and one for the basket service's dashboard. + +### Named endpoints using configuration + +With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": + +```json +{ + "Services": { + "basket": [ + "10.2.3.4:8080", /* the default endpoint, when resolving http://basket */ + "_dashboard.10.2.3.4:9999" /* the "dashboard" endpoint, resolved via http://_dashboard.basket */ + ] + } +} +``` + +### Named endpoints in .NET Aspire + +.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: + +```csharp +var builder = DistributedApplication.CreateBuilder(args); + +var basket = builder.AddProject("basket") + .WithServiceBinding(hostPort: 9999, scheme: "http", name: "admin"); + +var adminDashboard = builder.AddProject("admin-dashboard") + .WithReference(basket.GetEndPoint("admin")); + +var frontend = builder.AddProject("frontend") + .WithReference(basket); +``` + +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: + +```csharp + +// The preceding code is the same as in the above sample + +var frontend = builder.AddProject("frontend") + .WithReference(basket.GetEndpoint("http")); +``` + +### Named endpoints in Kubernetes using DNS SRV + +When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". + +```yml +apiVersion: v1 +kind: Service +metadata: + name: basket +spec: + selector: + name: basket-service + clusterIP: None + ports: + - name: default + port: 8080 + - name: dashboard + port: 8888 +``` + +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: + +```csharp +builder.Services.AddServiceDiscoveryCore(); +builder.Services.AddDnsSrvServiceEndPointResolver(); +``` + +The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. + +As in the previous example, add service discovery to an `HttpClient` for the basket service: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://basket")); +``` + +Similarly, the "dashboard" endpoint can be targeted as follows: + +```csharp +builder.Services.AddHttpClient( + static client => client.BaseAddress = new("http://_dashboard.basket")); +``` + +### Named endpoints in Azure Container Apps + +Named endpoints are not currently supported for services deployed to Azure Container Apps. + +## Feedback & contributing + +https://github.com/dotnet/aspire From aaf3fbf0bc78cb179b0aeb356efcd1b9da56c46a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 14 Nov 2023 15:43:08 -0600 Subject: [PATCH 18/77] Fix NuGet Package Icons (#817) --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 0ec16344e2a..edafcce1914 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -5,7 +5,7 @@ true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index a3036bf028f..7fed469c4d8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -5,7 +5,7 @@ true true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 729e5f9f405..c5145e06528 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true true Provides extensions for service discovery for the YARP reverse proxy. - true + $(DefaultDotnetIconFullPath) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 81a21ac841a..816a68bc78b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -5,7 +5,7 @@ true true Provides extensions to HttpClient that enable service discovery based on configuration. - true + $(DefaultDotnetIconFullPath) From 1525b005033e5722385969a274c4d40c31ae0198 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 16 Nov 2023 08:29:39 -0800 Subject: [PATCH 19/77] Service Discovery: fix shutdown blocking indefinitely in some cases (#880) * Service Discovery: fix shutdown blocking indefinitely in some cases * Update src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs Co-authored-by: Eric Erhardt * Review feedback --------- Co-authored-by: Eric Erhardt --- .../Http/HttpServiceEndPointResolver.cs | 8 +- .../ServiceEndPointResolverRegistry.cs | 8 +- .../ServiceEndPointResolverTests.cs | 75 +++++++++++++++++++ 3 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index e9e80bc76bb..da7fee6dd47 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -122,7 +122,7 @@ private void CleanupResolvers() { lock (_lock) { - if (_cleanupTask is { IsCompleted: true }) + if (_cleanupTask is null or { IsCompleted: true }) { _cleanupTask = CleanupResolversAsyncCore(); } @@ -159,9 +159,9 @@ private sealed class ResolverEntry : IAsyncDisposable { private readonly ServiceEndPointResolver _resolver; private readonly IServiceEndPointSelector _selector; - private const ulong CountMask = unchecked((ulong)-1); - private const ulong RecentUseFlag = 1UL << 61; - private const ulong DisposingFlag = 1UL << 62; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs index 8d039f2d74d..fb71bb9b85c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs @@ -121,7 +121,7 @@ private void CleanupResolvers() { lock (_lock) { - if (_cleanupTask is { IsCompleted: true }) + if (_cleanupTask is null or { IsCompleted: true }) { _cleanupTask = CleanupResolversAsyncCore(); } @@ -155,9 +155,9 @@ private ResolverEntry CreateResolver(string serviceName) private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable { private readonly ServiceEndPointResolver _resolver = resolver; - private const ulong CountMask = unchecked((ulong)-1); - private const ulong RecentUseFlag = 1UL << 61; - private const ulong DisposingFlag = 1UL << 62; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index c435f965fe6..ff964eb28f6 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Primitives; using Microsoft.Extensions.ServiceDiscovery.Abstractions; +using Microsoft.Extensions.ServiceDiscovery.Http; using Xunit; namespace Microsoft.Extensions.ServiceDiscovery.Tests; @@ -135,6 +136,80 @@ public async Task ResolveServiceEndPoint() } } + [Fact] + public async Task ResolveServiceEndPointOneShot() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .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 ip = Assert.IsType(sep.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + await services.DisposeAsync().ConfigureAwait(false); + } + + [Fact] + public async Task ResolveHttpServiceEndPointOneShot() + { + var cts = new[] { new CancellationTokenSource() }; + var innerResolver = new FakeEndPointResolver( + resolveAsync: (collection, ct) => + { + collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + + if (cts[0].Token.IsCancellationRequested) + { + cts[0] = new(); + collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + } + return default; + }, + disposeAsync: () => default); + var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var services = new ServiceCollection() + .AddSingleton(fakeResolverProvider) + .AddServiceDiscoveryCore() + .BuildServiceProvider(); + var selectorProvider = services.GetRequiredService(); + var resolverProvider = services.GetRequiredService(); + await using var resolver = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, TimeProvider.System); + + Assert.NotNull(resolver); + var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); + var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endPoint); + var ip = Assert.IsType(endPoint.EndPoint); + Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); + Assert.Equal(8080, ip.Port); + + await services.DisposeAsync().ConfigureAwait(false); + } + [Fact] public async Task ResolveServiceEndPoint_ThrowOnReload() { From b51c429ffbe64e673c9ed18343e84382221dbbef Mon Sep 17 00:00:00 2001 From: Arvin Kahbazi Date: Thu, 30 Nov 2023 22:38:48 +0330 Subject: [PATCH 20/77] Use ValueStopWatch (#1148) --- .../Http/ResolvingHttpClientHandler.cs | 6 +++--- .../Http/ResolvingHttpDelegatingHandler.cs | 6 +++--- .../Microsoft.Extensions.ServiceDiscovery.csproj | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 50a86722feb..52ff1d4494d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.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. -using System.Diagnostics; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -19,7 +19,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + var responseDuration = ValueStopwatch.StartNew(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -39,7 +39,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(responseDuration.GetElapsedTime(), 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 41331ef5215..8c0d67f41bd 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,8 +1,8 @@ // 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.Internal; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -39,7 +39,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = Stopwatch.StartNew(); // TODO: use a non-allocating stopwatch here. + var responseDuration = ValueStopwatch.StartNew(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -59,7 +59,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.Elapsed, error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + epHealth?.ReportHealth(responseDuration.GetElapsedTime(), 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 816a68bc78b..1fa156be1d4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,6 +10,7 @@ + From 7050e5a7593c3d643d28dc0ab294fa585c070ae2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Thu, 30 Nov 2023 19:50:22 -0800 Subject: [PATCH 21/77] Make behavior of IHttpClientBuilder.UseServiceDiscovery overloads consistent (#1160) --- .../Http/HttpClientBuilderExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 7e9df30b770..156a08b7a6c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -34,6 +34,10 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt return new ResolvingHttpDelegatingHandler(registry); }); + // 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; } From 50f8ab2f4dc89c88274dbd3710072966a4c02f46 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Fri, 22 Dec 2023 09:04:49 -0800 Subject: [PATCH 22/77] Rename WithServiceBinding to WithEndpoint (#1484) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 7ae0e4872c8..8bece9644ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -205,7 +205,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified var builder = DistributedApplication.CreateBuilder(args); var basket = builder.AddProject("basket") - .WithServiceBinding(hostPort: 9999, scheme: "http", name: "admin"); + .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") .WithReference(basket.GetEndPoint("admin")); From e9ee54076113f15f64076aa0bc8f8f55658ecddd Mon Sep 17 00:00:00 2001 From: Meir Blachman Date: Wed, 10 Jan 2024 20:01:24 +0200 Subject: [PATCH 23/77] Add comment to explain DisableGrpcLoadBalancingFilter (#1163) --- .../Http/HttpClientBuilderExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 156a08b7a6c..239db961e26 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -105,6 +105,7 @@ public Action Configure(Action Date: Tue, 16 Jan 2024 01:35:21 +0300 Subject: [PATCH 24/77] RoundRobinServiceEndPointSelectorProvider (#1661) Co-authored-by: Alexander Kucherov --- .../LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs index ed1e79d7416..40d9ce7845c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs @@ -11,7 +11,7 @@ public class RoundRobinServiceEndPointSelectorProvider : IServiceEndPointSelecto /// /// Gets a shared instance of this class. /// - public static RandomServiceEndPointSelectorProvider Instance { get; } = new(); + public static RoundRobinServiceEndPointSelectorProvider Instance { get; } = new(); /// public IServiceEndPointSelector CreateSelector() => new RoundRobinServiceEndPointSelector(); From 6ad2ba6c173030fb3e6973f034a7a8f4193743e2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 22 Jan 2024 20:02:14 -0800 Subject: [PATCH 25/77] Remove IServiceEndPointResolver.DisplayName property and use ToString() instead (#1761) --- .../IServiceEndPointResolver.cs | 5 ----- .../DnsServiceEndPointResolver.cs | 2 +- .../DnsServiceEndPointResolverBase.cs | 8 ++++++-- .../DnsSrvServiceEndPointResolver.cs | 5 ++++- .../Configuration/ConfigurationServiceEndPointResolver.cs | 3 --- .../PassThrough/PassThroughServiceEndPointResolver.cs | 4 ++-- .../ServiceEndPointResolver.Log.cs | 2 +- .../ServiceEndPointResolverFactory.Log.cs | 2 +- .../ServiceEndPointResolverTests.cs | 2 -- 9 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs index 3cbb9b6c491..c228847c568 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs @@ -8,11 +8,6 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// public interface IServiceEndPointResolver : IAsyncDisposable { - /// - /// Gets the diagnostic display name for this resolver. - /// - string DisplayName { get; } - /// /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index b0d30530c72..814c566a23e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -23,7 +23,7 @@ internal sealed partial class DnsServiceEndPointResolver( string IHostNameFeature.HostName => hostName; /// - public override string DisplayName => "DNS"; + public override string ToString() => "DNS"; protected override async Task ResolveAsyncCore() { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 238ace927fa..ae4ba97c2a2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -44,16 +44,18 @@ protected DnsServiceEndPointResolverBase( _lastChangeToken = new CancellationChangeToken(cancellation.Token); } - public abstract string DisplayName { get; } - private TimeSpan ElapsedSinceRefresh => _timeProvider.GetElapsedTime(_lastRefreshTimeStamp); protected string ServiceName { get; } protected abstract double RetryBackOffFactor { get; } + protected abstract TimeSpan MinRetryPeriod { get; } + protected abstract TimeSpan MaxRetryPeriod { get; } + protected abstract TimeSpan DefaultRefreshPeriod { get; } + protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// @@ -116,7 +118,9 @@ private async Task ResolveAsyncInternal() } 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) { lock (_lock) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index ce8ae159764..65ec30f8f29 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -20,11 +20,14 @@ internal sealed partial class DnsSrvServiceEndPointResolver( TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; + protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; + protected override TimeSpan MaxRetryPeriod => options.CurrentValue.MaxRetryPeriod; + protected override TimeSpan DefaultRefreshPeriod => options.CurrentValue.DefaultRefreshPeriod; - public override string DisplayName => "DNS SRV"; + public override string ToString() => "DNS SRV"; string IHostNameFeature.HostName => hostName; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index fd382487551..89ebce80ed4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -49,9 +49,6 @@ public ConfigurationServiceEndPointResolver( _options = options; } - /// - public string DisplayName => "Configuration"; - /// public ValueTask DisposeAsync() => default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index 4ee6b0dd32d..02349c74593 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -12,8 +12,6 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver { - public string DisplayName => "Pass-through"; - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) { if (endPoints.EndPoints.Count != 0) @@ -29,4 +27,6 @@ public ValueTask ResolveAsync(ServiceEndPointCollectionSource } public ValueTask DisposeAsync() => default; + + public override string ToString() => "Pass-through"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs index 274d471030d..bbde620ac1e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs @@ -30,7 +30,7 @@ static string GetEndPointString(ServiceEndPoint ep) { if (ep.Features.Get() is { } resolver) { - return $"{ep.GetEndPointString()} ({resolver.DisplayName})"; + return $"{ep.GetEndPointString()} ({resolver})"; } return ep.GetEndPointString(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs index 23d4b03cd67..fdf08f4fa6e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -16,7 +16,7 @@ public static void CreatingResolver(ILogger logger, string serviceName, List r.DisplayName))); + ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); } } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index ff964eb28f6..64972b37fb8 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -77,8 +77,6 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver { - public string DisplayName => "Fake"; - public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } From 43a49c3ef17bcea8b15c9f6d8968dc223d085704 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 13 Feb 2024 17:41:51 -0600 Subject: [PATCH 26/77] Enable XML doc error (#2191) * Enable XML doc error Fail the build when a public member is not documented. Since there are so many violations, for the current ones that weren't straight forward, I added a TODO for someone more familiar with the API to add the comments. * Add XML docs for new APIs --- .../ResolutionStatus.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs index 152571bbb74..04eec95dc63 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs @@ -92,6 +92,7 @@ public bool Equals(ResolutionStatus other) => StatusCode == other.StatusCode && /// 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}]", From 8740979273a910a8115ba6255c42f77ab2d4a74b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 23 Feb 2024 06:43:45 -0800 Subject: [PATCH 27/77] Service Discovery: set port to 0 when scheme is not present, not -1 (#1885) --- .../Internal/ServiceNameParts.cs | 3 ++- ...PassThroughServiceEndPointResolverTests.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index 0d310190a33..f9ab6ff6c2b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -46,6 +46,7 @@ static ServiceNameParts Create(Uri uri, bool hasScheme) 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]; @@ -62,7 +63,7 @@ static ServiceNameParts Create(Uri uri, bool hasScheme) } } - return new(host, endPointName, uri.Port); + return new(host, endPointName, port); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 9d3e6ac17e7..9325336f319 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -109,4 +109,26 @@ public async Task ResolveServiceEndPoint_Fallback() Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPoints[0].EndPoint); } } + + // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. + [Fact] + public async Task ResolveServiceEndPoint_Fallback_NoScheme() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:0"] = "http://localhost:8080", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscovery() // Adds the configuration and pass-through providers. + .BuildServiceProvider(); + + var resolver = services.GetRequiredService(); + var endPoints = await resolver.GetEndPointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); + } } From c85b3ef835e9b472e738d6a4d0b2228c5a1e82aa Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:04:14 -0800 Subject: [PATCH 28/77] Rename key Service Discovery types (#1877) * Service Discovery refactoring * Rename ServiceEndPointResolver to ServiceEndPointWatcher * Rename ServiceEndPointResolverRegistry to ServiceEndPointResolver * Rename IServiceEndPointResolver to IServiceEndPointProvider --- ...esolver.cs => IServiceEndPointProvider.cs} | 6 +- .../IServiceEndPointResolverProvider.cs | 6 +- .../DnsServiceEndPointResolver.cs | 2 +- .../DnsServiceEndPointResolverBase.Log.cs | 2 +- .../DnsServiceEndPointResolverBase.cs | 2 +- .../DnsServiceEndPointResolverProvider.cs | 4 +- .../DnsSrvServiceEndPointResolver.cs | 2 +- .../DnsSrvServiceEndPointResolverProvider.cs | 4 +- .../ServiceDiscoveryDestinationResolver.cs | 6 +- .../ConfigurationServiceEndPointResolver.cs | 4 +- ...gurationServiceEndPointResolverProvider.cs | 2 +- .../HostingExtensions.cs | 2 +- .../Http/HttpClientBuilderExtensions.cs | 1 - .../Http/HttpServiceEndPointResolver.cs | 10 +- .../PassThroughServiceEndPointResolver.cs | 4 +- ...sThroughServiceEndPointResolverProvider.cs | 2 +- .../ServiceEndPointResolver.cs | 441 ++++++------------ .../ServiceEndPointResolverFactory.Log.cs | 4 +- .../ServiceEndPointResolverFactory.cs | 14 +- .../ServiceEndPointResolverOptions.cs | 2 +- .../ServiceEndPointResolverRegistry.cs | 240 ---------- ...r.Log.cs => ServiceEndPointWatcher.Log.cs} | 4 +- .../ServiceEndPointWatcher.cs | 383 +++++++++++++++ .../DnsSrvServiceEndPointResolverTests.cs | 6 +- ...nfigurationServiceEndPointResolverTests.cs | 8 +- ...PassThroughServiceEndPointResolverTests.cs | 8 +- .../ServiceEndPointResolverTests.cs | 16 +- 27 files changed, 597 insertions(+), 588 deletions(-) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolver.cs => IServiceEndPointProvider.cs} (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.Log.cs => ServiceEndPointWatcher.Log.cs} (94%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index c228847c568..3b369a97850 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs @@ -4,12 +4,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Functionality for resolving endpoints for a service. +/// Provides details about a service's endpoints. /// -public interface IServiceEndPointResolver : IAsyncDisposable +public interface IServiceEndPointProvider : IAsyncDisposable { /// - /// Attempts to resolve the endpoints for the service which this instance is configured to resolve endpoints for. + /// Resolves the endpoints for the service. /// /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs index 9ad9e3ae7b8..51343a53697 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs @@ -6,15 +6,15 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Creates instances. +/// Creates instances. /// public interface IServiceEndPointResolverProvider { /// - /// Tries to create an instance for the specified . + /// Tries to create an instance for the specified . /// /// The service to create the resolver for. /// The resolver. /// if the resolver was created, otherwise. - bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver); + bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 814c566a23e..4a8350483eb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs @@ -34,7 +34,7 @@ protected override async Task ResolveAsyncCore() foreach (var address in addresses) { var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs index c3384d20e19..cd664215aa9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndPointResolverBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index ae4ba97c2a2..516c8ed1f69 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// /// A service end point resolver that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointResolver +internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider { private readonly object _lock = new(); private readonly ILogger _logger; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index fc5f707e411..04eec3ac52c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Provides instances which resolve endpoints from DNS. +/// Provides instances which resolve endpoints from DNS. /// /// /// Initializes a new instance. @@ -24,7 +24,7 @@ internal sealed partial class DnsServiceEndPointResolverProvider( TimeProvider timeProvider) : IServiceEndPointResolverProvider { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { if (!ServiceNameParts.TryParse(serviceName, out var parts)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 65ec30f8f29..97a0d47d028 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs @@ -90,7 +90,7 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index e5a7e23710e..ced6e2c4a5f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Provides instances which resolve endpoints from DNS using SRV queries. +/// Provides instances which resolve endpoints from DNS using SRV queries. /// /// /// Initializes a new instance. @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [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 diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index fbcef8c72ad..e35be5d629b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -13,8 +13,8 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// /// Initializes a new instance. /// -/// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolverRegistry registry) : IDestinationResolver +/// The endpoint resolver registry. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,7 +54,7 @@ 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 registry.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); + var endPoints = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); var results = new List<(string Name, DestinationConfig Config)>(endPoints.Count); var uriBuilder = new UriBuilder(originalUri); var healthUri = originalConfig.Health is { Length: > 0 } health ? new Uri(health) : null; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 89ebce80ed4..8ffb3de4039 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// A service endpoint resolver that uses configuration to resolve endpoints. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointResolver, IHostNameFeature +internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature { private readonly string _serviceName; private readonly string? _endpointName; @@ -147,7 +147,7 @@ static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => private ServiceEndPoint CreateEndPoint(EndPoint endPoint) { var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); + serviceEndPoint.Features.Set(this); if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) { serviceEndPoint.Features.Set(this); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index affe3a655ed..638c3e6d640 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -24,7 +24,7 @@ public class ConfigurationServiceEndPointResolverProvider( private readonly ILogger _logger = loggerFactory.CreateLogger(); /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); return true; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 94f806a4016..5326d5a2935 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -39,7 +39,7 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services.TryAddSingleton(static sp => TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 239db961e26..0a507fb2f0b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -53,7 +53,6 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var selectorProvider = services.GetRequiredService(); var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index da7fee6dd47..a135a2b02ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -10,13 +10,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, IServiceEndPointSelectorProvider selectorProvider, TimeProvider timeProvider) : IAsyncDisposable +public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolverFactory, IServiceEndPointSelectorProvider selectorProvider, 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 _resolverProvider = resolverProvider; + private readonly ServiceEndPointResolverFactory _resolverFactory = resolverFactory; private readonly IServiceEndPointSelectorProvider _selectorProvider = selectorProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -148,7 +148,7 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateResolver(serviceName); + var resolver = _resolverFactory.CreateResolver(serviceName); var selector = _selectorProvider.CreateSelector(); var result = new ResolverEntry(resolver, selector); resolver.Start(); @@ -157,7 +157,7 @@ private ResolverEntry CreateResolver(string serviceName) private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointResolver _resolver; + private readonly ServiceEndPointWatcher _resolver; private readonly IServiceEndPointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; @@ -165,7 +165,7 @@ private sealed class ResolverEntry : IAsyncDisposable private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointResolver resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) { _resolver = resolver; _selector = selector; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index 02349c74593..ab0ea286b68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// /// Service endpoint resolver which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointResolver +internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider { public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) { @@ -21,7 +21,7 @@ public ValueTask ResolveAsync(ServiceEndPointCollectionSource Log.UsingPassThrough(logger, serviceName); var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); + ep.Features.Set(this); endPoints.EndPoints.Add(ep); return new(ResolutionStatus.Success); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 24028f24ee5..32a64b8d350 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointResolverProvider { /// - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index b8356d56af5..965363dea9e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -1,383 +1,250 @@ // 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.Concurrent; 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; namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Resolves endpoints for a specified service. +/// Resolves service names to collections of endpoints. /// -public sealed partial class ServiceEndPointResolver( - IServiceEndPointResolver[] resolvers, - ILogger logger, - string serviceName, - TimeProvider timeProvider, - IOptions options) : IAsyncDisposable +public sealed class ServiceEndPointResolver : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointResolver)state!).RefreshAsync(force: true); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ILogger _logger = logger; - private readonly TimeProvider _timeProvider = timeProvider; - private readonly ServiceEndPointResolverOptions _options = options.Value; - private readonly IServiceEndPointResolver[] _resolvers = resolvers; - private readonly CancellationTokenSource _disposalCancellation = new(); - private ITimer? _pollingTimer; - private ServiceEndPointCollection? _cachedEndPoints; - private Task _refreshTask = Task.CompletedTask; - private volatile CacheStatus _cacheState; + private readonly ServiceEndPointResolverFactory _resolverProvider; + private readonly TimeProvider _timeProvider; + private readonly ConcurrentDictionary _resolvers = new(); + private ITimer? _cleanupTimer; + private Task? _cleanupTask; + private bool _disposed; /// - /// Gets the service name. + /// Initializes a new instance of the class. /// - public string ServiceName { get; } = serviceName; - - /// - /// Gets or sets the action called when endpoints are updated. - /// - public Action? OnEndPointsUpdated { get; set; } - - /// - /// Starts the endpoint resolver. - /// - public void Start() + /// The resolver factory. + /// The time provider. + internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) { - ThrowIfNoResolvers(); - _ = RefreshAsync(force: false); + _resolverProvider = resolverProvider; + _timeProvider = timeProvider; } /// - /// Returns a collection of resolved endpoints for the service. + /// Resolves and returns service endpoints for the specified service. /// + /// The service name. /// The cancellation token. - /// A collection of resolved endpoints for the service. - public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) + /// The resolved service endpoints. + public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { - ThrowIfNoResolvers(); + ArgumentNullException.ThrowIfNull(serviceName); + ObjectDisposedException.ThrowIf(_disposed, this); - // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) - { - return new ValueTask(cached); - } + EnsureCleanupTimerStarted(); - // 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) + while (true) { - ServiceEndPointCollection? result; - do - { - await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; - } while (result is null); - return result; - } - } + ObjectDisposedException.ThrowIf(_disposed, this); + var resolver = _resolvers.GetOrAdd( + serviceName, + static (name, self) => self.CreateResolver(name), + this); - // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation - private Task RefreshAsync(bool force) - { - lock (_lock) - { - // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + if (valid) { - // Indicate that the cache is being updated and start a new refresh task. - _cacheState = CacheStatus.Refreshing; - - // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. - var restoreFlow = false; - try + if (result is null) { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _refreshTask = RefreshAsyncInternal(); + throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); } - finally - { - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } - } - } - return _refreshTask; + return result; + } } } - private async Task RefreshAsyncInternal() + private void EnsureCleanupTimerStarted() { - await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); - var cancellationToken = _disposalCancellation.Token; - Exception? error = null; - ServiceEndPointCollection? newEndPoints = null; - CacheStatus newCacheState; - ResolutionStatus status = ResolutionStatus.Success; - while (true) - { - try - { - 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); - } - - var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); - var statusCode = status.StatusCode; - if (statusCode != ResolutionStatusCode.Success) - { - 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) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception ?? new OperationCanceledException(); - break; - } - else if (statusCode is ResolutionStatusCode.Error) - { - newCacheState = CacheStatus.Invalid; - error = status.Exception; - break; - } - } - - lock (_lock) - { - // 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 => _ = ((ServiceEndPointResolver)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; - } - } - catch (Exception exception) - { - error = exception; - newCacheState = CacheStatus.Invalid; - SchedulePollingTimer(); - status = CombineStatus(status, ResolutionStatus.FromException(exception)); - break; - } - } - - // If there was an error, the cache must be invalid. - Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, - // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but - // that will have more overhead in the common case. - if (newCacheState is CacheStatus.Valid) - { - Interlocked.Exchange(ref _cachedEndPoints, null); - } - - if (OnEndPointsUpdated is { } callback) + if (_cleanupTimer is not null) { - callback(new(newEndPoints, status)); + return; } lock (_lock) { - if (newCacheState is CacheStatus.Valid) + if (_cleanupTimer is not null) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + return; } - _cacheState = newCacheState; - } - - if (error is not null) - { - Log.ResolutionFailed(_logger, error, ServiceName); - ExceptionDispatchInfo.Throw(error); - } - else if (newEndPoints is not null) - { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); - } - } - - private void SchedulePollingTimer() - { - lock (_lock) - { - if (_pollingTimer is null) + // Don't capture the current ExecutionContext and its AsyncLocals onto the timer + var restoreFlow = false; + try { - _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _cleanupTimer = _timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); } - else + finally { - _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + // Restore the current ExecutionContext + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } } } } - private static ResolutionStatus CombineStatus(ResolutionStatus existing, ResolutionStatus newStatus) + /// + public async ValueTask DisposeAsync() { - if (existing.StatusCode > newStatus.StatusCode) + lock (_lock) { - return existing; + _disposed = true; + _cleanupTimer?.Dispose(); + _cleanupTimer = null; } - 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 + foreach (var resolver in _resolvers) { - exception = existing.Exception ?? newStatus.Exception; + await resolver.Value.DisposeAsync().ConfigureAwait(false); } - var message = code switch + _resolvers.Clear(); + if (_cleanupTask is not null) { - 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); - } + await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } } - /// - public async ValueTask DisposeAsync() + private void CleanupResolvers() { lock (_lock) { - if (_pollingTimer is { } timer) + if (_cleanupTask is null or { IsCompleted: true }) { - _pollingTimer = null; - timer.Dispose(); + _cleanupTask = CleanupResolversAsyncCore(); } } + } - _disposalCancellation.Cancel(); - if (_refreshTask is { } task) + private async Task CleanupResolversAsyncCore() + { + List? cleanupTasks = null; + foreach (var (name, resolver) in _resolvers) { - await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) + { + cleanupTasks ??= new(); + cleanupTasks.Add(resolver.DisposeAsync().AsTask()); + } } - - foreach (var resolver in _resolvers) + if (cleanupTasks is not null) { - await resolver.DisposeAsync().ConfigureAwait(false); + await Task.WhenAll(cleanupTasks).ConfigureAwait(false); } } - private enum CacheStatus + private ResolverEntry CreateResolver(string serviceName) { - Invalid, - Refreshing, - Valid + var resolver = _resolverProvider.CreateResolver(serviceName); + resolver.Start(); + return new ResolverEntry(resolver); } - private static async Task WaitForPendingChangeToken(IChangeToken changeToken, TimeSpan pollPeriod, CancellationToken cancellationToken) + private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable { - if (changeToken.HasChanged) + private readonly ServiceEndPointWatcher _resolver = resolver; + private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); + private const ulong RecentUseFlag = 1UL << 62; + private const ulong DisposingFlag = 1UL << 63; + private ulong _status; + private TaskCompletionSource? _onDisposed; + + public string ServiceName => _resolver.ServiceName; + + public bool CanExpire() { - return; - } + // Read the status, clearing the recent use flag in the process. + var status = Interlocked.And(ref _status, ~RecentUseFlag); - TaskCompletionSource completion = new(TaskCreationOptions.RunContinuationsAsynchronously); - IDisposable? changeTokenRegistration = null; - IDisposable? cancellationRegistration = null; - IDisposable? pollPeriodRegistration = null; - CancellationTokenSource? timerCancellation = null; + // The instance can be expired if there are no concurrent callers and the recent use flag was not set. + return (status & (CountMask | RecentUseFlag)) == 0; + } - try + public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) { - // Either wait for a callback or poll externally. - if (changeToken.ActiveChangeCallbacks) + try { - changeTokenRegistration = changeToken.RegisterChangeCallback(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + var status = Interlocked.Increment(ref _status); + if ((status & DisposingFlag) == 0) + { + // If the resolver is valid, resolve. + // We ensure that it will not be disposed while we are resolving. + var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endPoints); + } + else + { + return (false, default); + } } - else + finally { - timerCancellation = new(pollPeriod); - pollPeriodRegistration = timerCancellation.Token.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + // Set the recent use flag to prevent the instance from being disposed. + Interlocked.Or(ref _status, RecentUseFlag); + + // If we are the last concurrent request to complete and the Disposing flag has been set, + // dispose the resolver now. DisposeAsync was prevented by concurrent requests. + var status = Interlocked.Decrement(ref _status); + if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) + { + await DisposeAsyncCore().ConfigureAwait(false); + } } + } - if (cancellationToken.CanBeCanceled) + public async ValueTask DisposeAsync() + { + if (_onDisposed is null) { - cancellationRegistration = cancellationToken.UnsafeRegister(static state => ((TaskCompletionSource)state!).TrySetResult(), completion); + Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); } - await completion.Task.ConfigureAwait(false); - } - finally - { - changeTokenRegistration?.Dispose(); - cancellationRegistration?.Dispose(); - pollPeriodRegistration?.Dispose(); - timerCancellation?.Dispose(); + var status = Interlocked.Or(ref _status, DisposingFlag); + if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) + { + // If we are the one who flipped the Disposing flag and there are no concurrent requests, + // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. + await DisposeAsyncCore().ConfigureAwait(false); + } + else + { + await _onDisposed.Task.ConfigureAwait(false); + } } - } - private void ThrowIfNoResolvers() - { - if (_resolvers.Length == 0) + private async Task DisposeAsyncCore() { - ThrowNoResolversConfigured(); + try + { + await _resolver.DisposeAsync().ConfigureAwait(false); + } + finally + { + Debug.Assert(_onDisposed is not null); + _onDisposed.SetResult(); + } } } - - [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs index fdf08f4fa6e..d7835f26d08 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; -public partial class ServiceEndPointResolverFactory +partial class ServiceEndPointResolverFactory { private sealed partial class Log { [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) { if (logger.IsEnabled(LogLevel.Debug)) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs index ce020178406..c545c82e9e6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs @@ -9,29 +9,29 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Creates instances. +/// Creates service endpoint resolvers. /// public partial class ServiceEndPointResolverFactory( IEnumerable resolvers, - ILogger resolverLogger, + ILogger resolverLogger, IOptions options, TimeProvider timeProvider) { private readonly IServiceEndPointResolverProvider[] _resolverProviders = resolvers .Where(r => r is not PassThroughServiceEndPointResolverProvider) .Concat(resolvers.Where(static r => r is PassThroughServiceEndPointResolverProvider)).ToArray(); - private readonly ILogger _logger = resolverLogger; + private readonly ILogger _logger = resolverLogger; private readonly TimeProvider _timeProvider = timeProvider; private readonly IOptions _options = options; /// - /// Creates a instance for the provided service name. + /// Creates a service endpoint resolver for the provided service name. /// - public ServiceEndPointResolver CreateResolver(string serviceName) + public ServiceEndPointWatcher CreateResolver(string serviceName) { ArgumentNullException.ThrowIfNull(serviceName); - List? resolvers = null; + List? resolvers = null; foreach (var factory in _resolverProviders) { if (factory.TryCreateResolver(serviceName, out var resolver)) @@ -47,7 +47,7 @@ public ServiceEndPointResolver CreateResolver(string serviceName) } Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointResolver( + return new ServiceEndPointWatcher( resolvers: [.. resolvers], logger: _logger, serviceName: serviceName, diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs index 8a2b37a5048..415a2192c30 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// Options for . +/// Options for service endpoint resolvers. /// public sealed class ServiceEndPointResolverOptions { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs deleted file mode 100644 index fb71bb9b85c..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverRegistry.cs +++ /dev/null @@ -1,240 +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.Collections.Concurrent; -using System.Diagnostics; -using Microsoft.Extensions.ServiceDiscovery.Abstractions; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Resolves service names to collections of endpoints. -/// -/// The resolver factory. -/// The time provider. -public sealed class ServiceEndPointResolverRegistry(ServiceEndPointResolverFactory resolverProvider, TimeProvider timeProvider) : IAsyncDisposable -{ - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolverRegistry)s!).CleanupResolvers(); - private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); - - private readonly object _lock = new(); - private readonly ServiceEndPointResolverFactory _resolverProvider = resolverProvider; - private readonly ConcurrentDictionary _resolvers = new(); - private ITimer? _cleanupTimer; - private Task? _cleanupTask; - private bool _disposed; - - /// - /// Resolves and returns service endpoints for the specified service. - /// - /// The service name. - /// The cancellation token. - /// The resolved service endpoints. - public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) - { - ArgumentNullException.ThrowIfNull(serviceName); - ObjectDisposedException.ThrowIf(_disposed, this); - - EnsureCleanupTimerStarted(); - - while (true) - { - ObjectDisposedException.ThrowIf(_disposed, this); - var resolver = _resolvers.GetOrAdd( - serviceName, - static (name, self) => self.CreateResolver(name), - this); - - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - if (valid) - { - if (result is null) - { - throw new InvalidOperationException($"Unable to resolve endpoints for service {resolver.ServiceName}"); - } - - return result; - } - } - } - - private void EnsureCleanupTimerStarted() - { - if (_cleanupTimer is not null) - { - return; - } - - lock (_lock) - { - if (_cleanupTimer is not null) - { - return; - } - - // Don't capture the current ExecutionContext and its AsyncLocals onto the timer - var restoreFlow = false; - try - { - if (!ExecutionContext.IsFlowSuppressed()) - { - ExecutionContext.SuppressFlow(); - restoreFlow = true; - } - - _cleanupTimer = timeProvider.CreateTimer(s_cleanupCallback, this, s_cleanupPeriod, s_cleanupPeriod); - } - finally - { - // Restore the current ExecutionContext - if (restoreFlow) - { - ExecutionContext.RestoreFlow(); - } - } - } - } - - /// - public async ValueTask DisposeAsync() - { - lock (_lock) - { - _disposed = true; - _cleanupTimer?.Dispose(); - _cleanupTimer = null; - } - - foreach (var resolver in _resolvers) - { - await resolver.Value.DisposeAsync().ConfigureAwait(false); - } - - _resolvers.Clear(); - if (_cleanupTask is not null) - { - await _cleanupTask.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); - } - } - - private void CleanupResolvers() - { - lock (_lock) - { - if (_cleanupTask is null or { IsCompleted: true }) - { - _cleanupTask = CleanupResolversAsyncCore(); - } - } - } - - private async Task CleanupResolversAsyncCore() - { - List? cleanupTasks = null; - foreach (var (name, resolver) in _resolvers) - { - if (resolver.CanExpire() && _resolvers.TryRemove(name, out var _)) - { - cleanupTasks ??= new(); - cleanupTasks.Add(resolver.DisposeAsync().AsTask()); - } - } - if (cleanupTasks is not null) - { - await Task.WhenAll(cleanupTasks).ConfigureAwait(false); - } - } - - private ResolverEntry CreateResolver(string serviceName) - { - var resolver = _resolverProvider.CreateResolver(serviceName); - resolver.Start(); - return new ResolverEntry(resolver); - } - - private sealed class ResolverEntry(ServiceEndPointResolver resolver) : IAsyncDisposable - { - private readonly ServiceEndPointResolver _resolver = resolver; - private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); - private const ulong RecentUseFlag = 1UL << 62; - private const ulong DisposingFlag = 1UL << 63; - private ulong _status; - private TaskCompletionSource? _onDisposed; - - public string ServiceName => _resolver.ServiceName; - - public bool CanExpire() - { - // Read the status, clearing the recent use flag in the process. - var status = Interlocked.And(ref _status, ~RecentUseFlag); - - // The instance can be expired if there are no concurrent callers and the recent use flag was not set. - return (status & (CountMask | RecentUseFlag)) == 0; - } - - public async ValueTask<(bool Valid, ServiceEndPointCollection? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) - { - try - { - var status = Interlocked.Increment(ref _status); - if ((status & DisposingFlag) == 0) - { - // If the resolver is valid, resolve. - // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); - } - else - { - return (false, default); - } - } - finally - { - // Set the recent use flag to prevent the instance from being disposed. - Interlocked.Or(ref _status, RecentUseFlag); - - // If we are the last concurrent request to complete and the Disposing flag has been set, - // dispose the resolver now. DisposeAsync was prevented by concurrent requests. - var status = Interlocked.Decrement(ref _status); - if ((status & DisposingFlag) == DisposingFlag && (status & CountMask) == 0) - { - await DisposeAsyncCore().ConfigureAwait(false); - } - } - } - - public async ValueTask DisposeAsync() - { - if (_onDisposed is null) - { - Interlocked.CompareExchange(ref _onDisposed, new(TaskCreationOptions.RunContinuationsAsynchronously), null); - } - - var status = Interlocked.Or(ref _status, DisposingFlag); - if ((status & DisposingFlag) != DisposingFlag && (status & CountMask) == 0) - { - // If we are the one who flipped the Disposing flag and there are no concurrent requests, - // dispose the instance now. Concurrent requests are prevented from starting by the Disposing flag. - await DisposeAsyncCore().ConfigureAwait(false); - } - else - { - await _onDisposed.Task.ConfigureAwait(false); - } - } - - private async Task DisposeAsyncCore() - { - try - { - await _resolver.DisposeAsync().ConfigureAwait(false); - } - finally - { - Debug.Assert(_onDisposed is not null); - _onDisposed.SetResult(); - } - } - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index bbde620ac1e..17864811062 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; -public sealed partial class ServiceEndPointResolver +partial class ServiceEndPointWatcher { private sealed partial class Log { @@ -28,7 +28,7 @@ public static void ResolutionSucceeded(ILogger logger, string serviceName, Servi static string GetEndPointString(ServiceEndPoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } resolver) { return $"{ep.GetEndPointString()} ({resolver})"; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs new file mode 100644 index 00000000000..51e597ee89d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -0,0 +1,383 @@ +// 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.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; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Watches for updates to the collection of resolved endpoints for a specified service. +/// +public sealed partial class ServiceEndPointWatcher( + IServiceEndPointProvider[] resolvers, + ILogger logger, + string serviceName, + TimeProvider timeProvider, + 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 IServiceEndPointProvider[] _resolvers = resolvers; + private readonly CancellationTokenSource _disposalCancellation = new(); + private ITimer? _pollingTimer; + private ServiceEndPointCollection? _cachedEndPoints; + private Task _refreshTask = Task.CompletedTask; + private volatile CacheStatus _cacheState; + + /// + /// Gets the service name. + /// + public string ServiceName { get; } = serviceName; + + /// + /// Gets or sets the action called when endpoints are updated. + /// + public Action? OnEndPointsUpdated { get; set; } + + /// + /// Starts the endpoint resolver. + /// + public void Start() + { + ThrowIfNoResolvers(); + _ = RefreshAsync(force: false); + } + + /// + /// Returns a collection of resolved endpoints for the service. + /// + /// The cancellation token. + /// A collection of resolved endpoints for the service. + 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); + } + + // 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) + { + ServiceEndPointCollection? result; + do + { + await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); + result = _cachedEndPoints; + } while (result is null); + return result; + } + } + + // Ensures that there is a refresh operation running, if needed, and returns the task which represents the completion of the operation + private Task RefreshAsync(bool force) + { + lock (_lock) + { + // If the cache is invalid or needs invalidation, refresh the cache. + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + { + // Indicate that the cache is being updated and start a new refresh task. + _cacheState = CacheStatus.Refreshing; + + // Don't capture the current ExecutionContext and its AsyncLocals onto the callback. + var restoreFlow = false; + try + { + if (!ExecutionContext.IsFlowSuppressed()) + { + ExecutionContext.SuppressFlow(); + restoreFlow = true; + } + + _refreshTask = RefreshAsyncInternal(); + } + finally + { + if (restoreFlow) + { + ExecutionContext.RestoreFlow(); + } + } + } + + return _refreshTask; + } + } + + private async Task RefreshAsyncInternal() + { + await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); + var cancellationToken = _disposalCancellation.Token; + Exception? error = null; + ServiceEndPointCollection? newEndPoints = null; + CacheStatus newCacheState; + ResolutionStatus status = ResolutionStatus.Success; + while (true) + { + try + { + 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); + } + + var endPoints = ServiceEndPointCollectionSource.CreateServiceEndPointCollection(collection); + var statusCode = status.StatusCode; + if (statusCode != ResolutionStatusCode.Success) + { + 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) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception ?? new OperationCanceledException(); + break; + } + else if (statusCode is ResolutionStatusCode.Error) + { + newCacheState = CacheStatus.Invalid; + error = status.Exception; + break; + } + } + + lock (_lock) + { + // 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; + } + } + catch (Exception exception) + { + error = exception; + newCacheState = CacheStatus.Invalid; + SchedulePollingTimer(); + status = CombineStatus(status, ResolutionStatus.FromException(exception)); + break; + } + } + + // If there was an error, the cache must be invalid. + Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); + + // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task + // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // that will have more overhead in the common case. + if (newCacheState is CacheStatus.Valid) + { + Interlocked.Exchange(ref _cachedEndPoints, null); + } + + if (OnEndPointsUpdated is { } callback) + { + callback(new(newEndPoints, status)); + } + + lock (_lock) + { + if (newCacheState is CacheStatus.Valid) + { + Debug.Assert(newEndPoints is not null); + _cachedEndPoints = newEndPoints; + } + + _cacheState = newCacheState; + } + + if (error is not null) + { + Log.ResolutionFailed(_logger, error, ServiceName); + ExceptionDispatchInfo.Throw(error); + } + else if (newEndPoints is not null) + { + Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + } + } + + private void SchedulePollingTimer() + { + lock (_lock) + { + if (_pollingTimer is null) + { + _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); + } + else + { + _pollingTimer.Change(_options.RefreshPeriod, TimeSpan.Zero); + } + } + } + + 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() + { + lock (_lock) + { + if (_pollingTimer is { } timer) + { + _pollingTimer = null; + timer.Dispose(); + } + } + + _disposalCancellation.Cancel(); + if (_refreshTask is { } task) + { + await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); + } + + foreach (var resolver in _resolvers) + { + await resolver.DisposeAsync().ConfigureAwait(false); + } + } + + private enum CacheStatus + { + Invalid, + Refreshing, + 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) + { + ThrowNoResolversConfigured(); + } + } + + [DoesNotReturn] + private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7531eec0a48..bd69969199d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class DnsSrvServiceEndPointResolverTests { @@ -104,7 +104,7 @@ public async Task ResolveServiceEndPoint_Dns() .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -191,7 +191,7 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo }; var services = serviceCollection.BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index e695362dc5d..66bc1ab8d1b 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for and . -/// These also cover and by extension. +/// These also cover and by extension. /// public class ConfigurationServiceEndPointResolverTests { @@ -30,7 +30,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -70,7 +70,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -114,7 +114,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() .AddConfigurationServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://_grpc.basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 9325336f319..b29593265e9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// /// Tests for . -/// These also cover and by extension. +/// These also cover and by extension. /// public class PassThroughServiceEndPointResolverTests { @@ -26,7 +26,7 @@ public async Task ResolveServiceEndPoint_PassThrough() .AddPassThroughServiceEndPointResolver() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -58,7 +58,7 @@ public async Task ResolveServiceEndPoint_Superseded() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -92,7 +92,7 @@ public async Task ResolveServiceEndPoint_Fallback() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://catalog")).ConfigureAwait(false)) { Assert.NotNull(resolver); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 64972b37fb8..0628bedbe73 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// public class ServiceEndPointResolverTests { @@ -36,7 +36,7 @@ public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = new ServiceEndPointResolver([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); + var resolverFactory = new ServiceEndPointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceEndPointResolverOptions())); var exception = Assert.Throws(resolverFactory.Start); Assert.Equal("No service endpoint resolvers are configured.", exception.Message); exception = await Assert.ThrowsAsync(async () => await resolverFactory.GetEndPointsAsync()); @@ -65,9 +65,9 @@ public async Task UseServiceDiscovery_NoResolvers_Throws() 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) : IServiceEndPointResolverProvider { - public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointResolver? resolver) + public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(serviceName); @@ -75,7 +75,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi } } - private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointResolver + private sealed class FakeEndPointResolver(Func> resolveAsync, Func disposeAsync) : IServiceEndPointProvider { public ValueTask ResolveAsync(ServiceEndPointCollectionSource endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); @@ -106,7 +106,7 @@ public async Task ResolveServiceEndPoint() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); @@ -157,7 +157,7 @@ public async Task ResolveServiceEndPointOneShot() .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); var initialEndPoints = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); @@ -242,7 +242,7 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() .BuildServiceProvider(); var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; + ServiceEndPointWatcher resolver; await using ((resolver = resolverFactory.CreateResolver("http://basket")).ConfigureAwait(false)) { Assert.NotNull(resolver); From 07dd40bad112fb5b92c04e3b2bd7b24eebe16203 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:03:30 -0800 Subject: [PATCH 29/77] Fix PassThroughServiceEndPointResolverTests (#2451) --- .../PassThroughServiceEndPointResolverTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index b29593265e9..696061949d9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -127,7 +127,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); var endPoints = await resolver.GetEndPointsAsync("catalog", default); Assert.Equal(new DnsEndPoint("catalog", 0), endPoints[0].EndPoint); } From 2b01e48ecd7e26ec32c76478626b0a0e8c493d2b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:03:31 -0800 Subject: [PATCH 30/77] Service Discovery: allow multiple schemes to be specified in request (#2719) * Service Discovery: allow multiple schemes to be specified. Add endpoint name to configuration path. * Remove superfluous WriteServiceDiscoveryEnvironmentVariables method * Rename AllocatedEndpointAnnotation to AllocatedEndpoint and make it a property on EndpointAnnotation * Remove unnecessary members * Add endpoint from launch profile * Specify launch profile in test projects --- ...sions.ServiceDiscovery.Abstractions.csproj | 1 + .../UriEndPoint.cs | 30 +++ .../DnsServiceEndPointResolverProvider.cs | 14 +- .../DnsSrvServiceEndPointResolverProvider.cs | 15 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 4 - ...viceDiscoveryForwarderHttpClientFactory.cs | 6 +- ...onfigurationServiceEndPointResolver.Log.cs | 13 +- .../ConfigurationServiceEndPointResolver.cs | 225 +++++++++++------- ...igurationServiceEndPointResolverOptions.cs | 24 +- ...gurationServiceEndPointResolverProvider.cs | 15 +- .../HostingExtensions.cs | 5 + .../Http/HttpClientBuilderExtensions.cs | 6 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 94 +++++--- .../ServiceDiscoveryOptionsValidator.cs | 20 ++ .../Internal/ServiceNameParser.cs | 78 ++++++ .../Internal/ServiceNameParts.cs | 82 +------ ...crosoft.Extensions.ServiceDiscovery.csproj | 1 + ...sThroughServiceEndPointResolverProvider.cs | 42 +++- .../ServiceDiscoveryOptions.cs | 29 +++ .../DnsSrvServiceEndPointResolverTests.cs | 4 +- ...nfigurationServiceEndPointResolverTests.cs | 164 ++++++++++++- ...PassThroughServiceEndPointResolverTests.cs | 8 +- 23 files changed, 628 insertions(+), 258 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index edafcce1914..e98dc409e76 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -6,6 +6,7 @@ true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) + Microsoft.Extensions.ServiceDiscovery diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs new file mode 100644 index 00000000000..6d3132da880 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.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.Net; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// An endpoint represented by a . +/// +/// The . +public sealed class UriEndPoint(Uri uri) : EndPoint +{ + /// + /// Gets the associated with this endpoint. + /// + public Uri Uri => uri; + + /// + public override bool Equals(object? obj) + { + return obj is UriEndPoint other && Uri.Equals(other.Uri); + } + + /// + public override int GetHashCode() => Uri.GetHashCode(); + + /// + public override string? ToString() => uri.ToString(); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 04eec3ac52c..8f676327d5f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs @@ -9,24 +9,16 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The time provider. internal sealed partial class DnsServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index ced6e2c4a5f..bf2f502c97a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -10,21 +10,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -/// -/// Provides instances which resolve endpoints from DNS using SRV queries. -/// -/// -/// Initializes a new instance. -/// -/// The options. -/// The logger. -/// The DNS client. -/// The time provider. internal sealed partial class DnsSrvServiceEndPointResolverProvider( IOptionsMonitor options, ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointResolverProvider + TimeProvider timeProvider, + ServiceNameParser parser) : IServiceEndPointResolverProvider { 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"); @@ -49,7 +40,7 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi return false; } - if (!ServiceNameParts.TryParse(serviceName, out var parts)) + if (!parser.TryParse(serviceName, out var parts)) { DnsServiceEndPointResolverBase.Log.ServiceNameIsNotUriOrDnsName(logger, serviceName); resolver = default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 7fed469c4d8..0d4c3cbeac2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index 0da7e8b55eb..d37e4f1407d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs @@ -1,6 +1,7 @@ // 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; @@ -10,11 +11,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; internal sealed class ServiceDiscoveryForwarderHttpClientFactory( TimeProvider timeProvider, IServiceEndPointSelectorProvider selectorProvider, - ServiceEndPointResolverFactory factory) : ForwarderHttpClientFactory + ServiceEndPointResolverFactory factory, + IOptions options) : ForwarderHttpClientFactory { protected override HttpMessageHandler WrapHandler(ForwarderHttpClientContext context, HttpMessageHandler handler) { var registry = new HttpServiceEndPointResolver(factory, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry, handler); + return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index b7e43172740..5916951c69d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs @@ -12,7 +12,7 @@ internal sealed partial class ConfigurationServiceEndPointResolver { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Skipping endpoint resolution for service '{ServiceName}': '{Reason}'.", EventName = "SkippedResolution")] + [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")] @@ -38,11 +38,11 @@ public static void EndPointNameMatchSelection(ILogger logger, string serviceName } } - [LoggerMessage(4, LogLevel.Debug, "Using configuration from path '{Path}' to resolve endpoints for service '{ServiceName}'.", EventName = "UsingConfigurationPath")] - public static partial void UsingConfigurationPath(ILogger logger, string path, string serviceName); + [LoggerMessage(4, 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 endpoints configured for service '{ServiceName}' from path '{Path}'.", EventName = "ConfigurationNotFound")] - internal static partial void ConfigurationNotFound(ILogger logger, string serviceName, string path); + [LoggerMessage(5, 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")] internal static partial void ConfiguredEndPoints(ILogger logger, string serviceName, string path, string configuredEndPoints); @@ -67,5 +67,8 @@ public static void ConfiguredEndPoints(ILogger logger, string serviceName, strin 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")] + internal static partial void EndpointConfigurationNotFound(ILogger logger, string endpointName, string serviceName, string path); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index 8ffb3de4039..dae054c9883 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs @@ -1,8 +1,8 @@ // 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 System.Net; -using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,12 +11,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// -/// A service endpoint resolver that uses configuration to resolve endpoints. +/// A service endpoint resolver that uses configuration to resolve resolved. /// internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature { + private const string DefaultEndPointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IOptions _options; @@ -28,16 +30,19 @@ internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEnd /// The configuration. /// The logger. /// The options. + /// The service name parser. public ConfigurationServiceEndPointResolver( string serviceName, IConfiguration configuration, ILogger logger, - IOptions options) + IOptions options, + ServiceNameParser parser) { - if (ServiceNameParts.TryParse(serviceName, out var parts)) + if (parser.TryParse(serviceName, out var parts)) { _serviceName = parts.Host; _endpointName = parts.EndPointName; + _schemes = parts.Schemes; } else { @@ -59,141 +64,193 @@ public ConfigurationServiceEndPointResolver( private ResolutionStatus ResolveInternal(ServiceEndPointCollectionSource endPoints) { - // Only add endpoints to the collection if a previous provider (eg, an override) did not add them. + // 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; } - var root = _configuration; - var baseSectionName = _options.Value.SectionName; - if (baseSectionName is { Length: > 0 }) - { - root = root.GetSection(baseSectionName); - } - // Get the corresponding config section. - var section = root.GetSection(_serviceName); - var configPath = GetConfigurationPath(baseSectionName); - Log.UsingConfigurationPath(_logger, configPath, _serviceName); + var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - return CreateNotFoundResponse(endPoints, configPath); + return CreateNotFoundResponse(endPoints, $"{_options.Value.SectionName}:{_serviceName}"); } - // Read the endpoint from the configuration. - // First check if there is a collection of sections - var children = section.GetChildren(); - if (children.Any()) + endPoints.AddChangeToken(section.GetReloadToken()); + + // Find an appropriate configuration section based on the input. + IConfigurationSection? namedSection = null; + string endpointName; + if (string.IsNullOrWhiteSpace(_endpointName)) { - var values = children.Select(c => c.Value!).Where(s => !string.IsNullOrEmpty(s)).ToList(); - if (values is { Count: > 0 }) + if (_schemes.Length == 0) { - // Use endpoint names if any of the values have an endpoint name set. - var parsedValues = ParseServiceNameParts(values, configPath); - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, parsedValues); - - var matchEndPointNames = !parsedValues.TrueForAll(static uri => string.IsNullOrEmpty(uri.EndPointName)); - Log.EndPointNameMatchSelection(_logger, _serviceName, matchEndPointNames); + // Use the section named "default". + endpointName = DefaultEndPointName; + namedSection = section.GetSection(endpointName); + } + else + { + // Set the ideal endpoint name for error messages. + endpointName = _schemes[0]; - foreach (var uri in parsedValues) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + foreach (var scheme in _schemes) { - // If either endpoint names are not in-use or the scheme matches, create an endpoint for this value. - if (!matchEndPointNames || EndPointNamesMatch(_endpointName, uri)) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - if (!ServiceNameParts.TryCreateEndPoint(uri, out var endPoint)) - { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); - } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); + endpointName = scheme; + namedSection = candidate; + break; } } } } - else if (section.Value is { } value && ServiceNameParts.TryParse(value, out var parsed)) + else + { + // Use the section corresponding to the endpoint name. + endpointName = _endpointName; + namedSection = section.GetSection(_endpointName); + } + + var configPath = $"{_options.Value.SectionName}:{_serviceName}:{endpointName}"; + if (!namedSection.Exists()) + { + return CreateNotFoundResponse(endPoints, configPath); + } + + List resolved = []; + Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); + + // Account for both the single and multi-value cases. + if (!string.IsNullOrWhiteSpace(namedSection.Value)) + { + // Single value case. + if (!TryAddEndPoint(resolved, namedSection, endpointName, out var error)) + { + return error; + } + } + else { - if (EndPointNamesMatch(_endpointName, parsed)) + // Multiple value case. + foreach (var child in namedSection.GetChildren()) { - if (!ServiceNameParts.TryCreateEndPoint(parsed, out var endPoint)) + if (!int.TryParse(child.Key, out _)) { - return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' is invalid.")); + return ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys.")); } - if (_logger.IsEnabled(LogLevel.Debug)) + if (!TryAddEndPoint(resolved, child, endpointName, out var error)) { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, [parsed]); + return error; } - - endPoints.EndPoints.Add(CreateEndPoint(endPoint)); } } - if (endPoints.EndPoints.Count == 0) + // Filter the resolved endpoints to only include those which match the specified scheme. + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - Log.ConfigurationNotFound(_logger, _serviceName, configPath); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } + } } - endPoints.AddChangeToken(section.GetReloadToken()); - return ResolutionStatus.Success; - - static bool EndPointNamesMatch(string? endPointName, ServiceNameParts parts) => - string.IsNullOrEmpty(parts.EndPointName) - || string.IsNullOrEmpty(endPointName) - || MemoryExtensions.Equals(parts.EndPointName, endPointName, StringComparison.OrdinalIgnoreCase); - } + var added = 0; + foreach (var ep in resolved) + { + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++added; + endPoints.EndPoints.Add(ep); + } + } + else + { + ++added; + endPoints.EndPoints.Add(ep); + } + } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) - { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + if (added == 0) { - serviceEndPoint.Features.Set(this); + return CreateNotFoundResponse(endPoints, configPath); } - return serviceEndPoint; - } + return ResolutionStatus.Success; - private ResolutionStatus CreateNotFoundResponse(ServiceEndPointCollectionSource endPoints, string configPath) - { - endPoints.AddChangeToken(_configuration.GetReloadToken()); - Log.ConfigurationNotFound(_logger, _serviceName, configPath); - return ResolutionStatus.CreateNotFound($"No configuration for the specified path '{configPath}' was found."); } - private string GetConfigurationPath(string? baseSectionName) + private bool TryAddEndPoint(List endPoints, IConfigurationSection section, string endpointName, out ResolutionStatus error) { - var configPath = new StringBuilder(); - if (baseSectionName is { Length: > 0 }) + var value = section.Value; + if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) { - configPath.Append(baseSectionName).Append(':'); + error = ResolutionStatus.FromException(new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.")); + return false; } - configPath.Append(_serviceName); - return configPath.ToString(); + endPoints.Add(CreateEndPoint(endPoint)); + error = default; + return true; } - private List ParseServiceNameParts(List input, string configPath) + private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) { - var results = new List(input.Count); - for (var i = 0; i < input.Count; ++i) + if (value.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri)) { - if (ServiceNameParts.TryParse(input[i], out var value)) + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(uri.Host, out var ip)) { - if (!results.Contains(value)) - { - results.Add(value); - } + endPoint = new IPEndPoint(ip, port); } else { - throw new InvalidOperationException($"The endpoint configuration '{input[i]}' from path '{configPath}[{i}]' for service '{_serviceName}' is invalid."); + endPoint = new DnsEndPoint(uri.Host, port); } } + else if (Uri.TryCreate(value, default, out uri)) + { + endPoint = new UriEndPoint(uri); + } + else + { + endPoint = null; + return false; + } + + return true; + } + + private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + { + var serviceEndPoint = ServiceEndPoint.Create(endPoint); + serviceEndPoint.Features.Set(this); + if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + { + serviceEndPoint.Features.Set(this); + } + + return serviceEndPoint; + } - return results; + 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs index 5e67a885ca9..c83589eb268 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs @@ -1,20 +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 Microsoft.Extensions.Options; + namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// Options for . /// -public class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndPointResolverOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". /// - public string? SectionName { get; set; } = "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; } + +internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + { + if (string.IsNullOrWhiteSpace(options.SectionName)) + { + return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); + } + + if (options.ApplyHostNameMetadata is null) + { + return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 638c3e6d640..472205f12f9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs @@ -5,28 +5,23 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.Abstractions; /// /// implementation that resolves services using . /// -/// The configuration. -/// The options. -/// The logger factory. -public class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndPointResolverProvider( IConfiguration configuration, IOptions options, - ILoggerFactory loggerFactory) : IServiceEndPointResolverProvider + ILogger logger, + ServiceNameParser parser) : IServiceEndPointResolverProvider { - private readonly IConfiguration _configuration = configuration; - private readonly IOptions _options = options; - private readonly ILogger _logger = loggerFactory.CreateLogger(); - /// public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - resolver = new ConfigurationServiceEndPointResolver(serviceName, _configuration, _logger, _options); + resolver = new ConfigurationServiceEndPointResolver(serviceName, configuration, logger, options, parser); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs index 5326d5a2935..a4ba9b63b31 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs @@ -4,8 +4,10 @@ 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.Internal; using Microsoft.Extensions.ServiceDiscovery.PassThrough; namespace Microsoft.Extensions.Hosting; @@ -36,6 +38,8 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection { services.AddOptions(); services.AddLogging(); + services.TryAddSingleton(); + services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(static sp => TimeProvider.System); services.TryAddSingleton(); services.TryAddSingleton(); @@ -63,6 +67,7 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS { services.AddServiceDiscoveryCore(); services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs index 0a507fb2f0b..c1c833de89f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs @@ -31,7 +31,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var timeProvider = services.GetService() ?? TimeProvider.System; var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. @@ -56,7 +57,8 @@ public static IHttpClientBuilder UseServiceDiscovery(this IHttpClientBuilder htt var selectorProvider = services.GetRequiredService(); var resolverProvider = services.GetRequiredService(); var registry = new HttpServiceEndPointResolver(resolverProvider, selectorProvider, timeProvider); - return new ResolvingHttpDelegatingHandler(registry); + var options = services.GetRequiredService>(); + return new ResolvingHttpDelegatingHandler(registry, options); }); // Configure the HttpClient to disable gRPC load balancing. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 52ff1d4494d..ba32139f6ab 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -9,9 +10,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver) : HttpClientHandler +public class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler { private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly ServiceDiscoveryOptions _options = options.Value; /// protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) @@ -23,7 +25,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 8c0d67f41bd..2ab3c8dc3ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -3,6 +3,7 @@ using System.Net; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Http; @@ -13,24 +14,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; public class ResolvingHttpDelegatingHandler : DelegatingHandler { private readonly HttpServiceEndPointResolver _resolver; + private readonly ServiceDiscoveryOptions _options; /// /// Initializes a new instance. /// /// The endpoint resolver. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver) + /// The service discovery options. + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) { _resolver = resolver; + _options = options.Value; } /// /// Initializes a new instance. /// /// The endpoint resolver. + /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; + _options = options.Value; } /// @@ -43,7 +49,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result); + request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; epHealth = result.Features.Get(); } @@ -64,37 +70,71 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint) + internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) { var endpoint = serviceEndPoint.EndPoint; - - string host; - int port; - switch (endpoint) + UriBuilder result; + if (endpoint is UriEndPoint { Uri: { } ep }) { - case IPEndPoint ip: - host = ip.Address.ToString(); - port = ip.Port; - break; - case DnsEndPoint dns: - host = dns.Host; - port = dns.Port; - break; - default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); - } + result = new UriBuilder(uri) + { + Scheme = ep.Scheme, + Host = ep.Host, + }; - var builder = new UriBuilder(uri) - { - Host = host, - }; + if (ep.Port > 0) + { + result.Port = ep.Port; + } - // Default to the default port for the scheme. - if (port > 0) + if (ep.AbsolutePath.Length > 1) + { + result.Path = $"{ep.AbsolutePath.TrimEnd('/')}/{uri.AbsolutePath.TrimStart('/')}"; + } + } + else { - builder.Port = port; + string host; + int port; + switch (endpoint) + { + case IPEndPoint ip: + host = ip.Address.ToString(); + port = ip.Port; + break; + case DnsEndPoint dns: + host = dns.Host; + port = dns.Port; + break; + default: + throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + } + + result = new UriBuilder(uri) + { + Host = host, + }; + + // Default to the default port for the scheme. + if (port > 0) + { + result.Port = port; + } + + if (uri.Scheme.IndexOf('+') > 0) + { + var scheme = uri.Scheme.Split('+')[0]; + if (options.AllowedSchemes.Equals(ServiceDiscoveryOptions.AllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + result.Scheme = scheme; + } + else + { + throw new InvalidOperationException($"The scheme '{scheme}' is not allowed."); + } + } } - return builder.Uri; + return result.Uri; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs new file mode 100644 index 00000000000..fae7bd6f4fc --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceDiscoveryOptionsValidator.cs @@ -0,0 +1,20 @@ +// 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.Internal; + +internal sealed class ServiceDiscoveryOptionsValidator : IValidateOptions +{ + public ValidateOptionsResult Validate(string? name, ServiceDiscoveryOptions options) + { + if (options.AllowedSchemes is null) + { + return ValidateOptionsResult.Fail("At least one allowed scheme must be specified."); + } + + return ValidateOptionsResult.Success; + } +} + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs new file mode 100644 index 00000000000..de047481872 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs @@ -0,0 +1,78 @@ +// 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs index f9ab6ff6c2b..f93729a40ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs @@ -1,15 +1,13 @@ // 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 System.Net; - namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal readonly struct ServiceNameParts : IEquatable { - public ServiceNameParts(string host, string? endPointName, int port) : this() + public ServiceNameParts(string[] schemePriority, string host, string? endPointName, int port) : this() { + Schemes = schemePriority; Host = host; EndPointName = endPointName; Port = port; @@ -17,86 +15,14 @@ public ServiceNameParts(string host, string? endPointName, int port) : this() 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 static 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; - - static 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; - if (hasScheme) - { - endPointName = uri.Scheme; - } - } - - return new(host, endPointName, port); - } - } - - public static bool TryCreateEndPoint(ServiceNameParts parts, [NotNullWhen(true)] out EndPoint? endPoint) - { - if (IPAddress.TryParse(parts.Host, out var ip)) - { - endPoint = new IPEndPoint(ip, parts.Port); - } - else if (!string.IsNullOrEmpty(parts.Host)) - { - endPoint = new DnsEndPoint(parts.Host, parts.Port); - } - else - { - endPoint = null; - return false; - } - - return true; - } - - public static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) - { - if (TryParse(serviceName, out var parts)) - { - return TryCreateEndPoint(parts, out serviceEndPoint); - } - - serviceEndPoint = null; - return false; - } - public override bool Equals(object? obj) => obj is ServiceNameParts other && Equals(other); public override int GetHashCode() => HashCode.Combine(EndPointName, Host, Port); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 1fa156be1d4..60ec55fe9df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index 32a64b8d350..b3a326010bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs @@ -5,7 +5,6 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.ServiceDiscovery.Abstractions; -using Microsoft.Extensions.ServiceDiscovery.Internal; namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; @@ -17,7 +16,7 @@ internal sealed class PassThroughServiceEndPointResolverProvider(ILogger public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) { - if (!ServiceNameParts.TryCreateEndPoint(serviceName, out var endPoint)) + if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); @@ -26,4 +25,43 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); return true; } + + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + { + if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) + { + serviceEndPoint = null; + return false; + } + + var uriHost = uri.Host; + var segmentSeparatorIndex = uriHost.IndexOf('.'); + string host; + if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') + { + // Skip the endpoint name, including its prefix ('_') and suffix ('.'). + host = uriHost[(segmentSeparatorIndex + 1)..]; + } + else + { + host = uriHost; + } + + var port = uri.Port > 0 ? uri.Port : 0; + if (IPAddress.TryParse(host, out var ip)) + { + serviceEndPoint = new IPEndPoint(ip, port); + } + else if (!string.IsNullOrEmpty(host)) + { + serviceEndPoint = new DnsEndPoint(host, port); + } + else + { + serviceEndPoint = null; + return false; + } + + return true; + } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs new file mode 100644 index 00000000000..d9510a3cf22 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.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. + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Options for configuring service discovery. +/// +public sealed class ServiceDiscoveryOptions +{ + /// + /// The value for which indicates that all 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]; +#pragma warning restore CA1825 // Avoid zero-length array allocations +#pragma warning restore IDE0300 // Simplify collection initialization + + /// + /// 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. + /// Schemes are not case-sensitive. + /// + public string[] AllowedSchemes { get; set; } = AllSchemes; +} + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index bd69969199d..7e2ef478ef7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs @@ -164,8 +164,8 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo { InitialData = new Dictionary { - ["services:basket:0"] = "localhost:8080", - ["services:basket:1"] = "remotehost:9090", + ["services:basket:http:0"] = "localhost:8080", + ["services:basket:http:1"] = "remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index 66bc1ab8d1b..f35ffa2026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs @@ -22,7 +22,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { - ["services:basket"] = "localhost:8080", + ["services:basket:http"] = "localhost:8080", }); var services = new ServiceCollection() .AddSingleton(config.Build()) @@ -52,6 +52,72 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } } + [Fact] + public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + { + // Try to resolve an http endpoint when only https is allowed. + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:foo:0"] = "http://localhost:8080", + ["services:basket:foo:1"] = "https://localhost", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .Configure(o => o.AllowedSchemes = ["https"]) + .BuildServiceProvider(); + 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)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(ResolutionStatus.Success, initialResult.Status); + Assert.Empty(initialResult.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)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + 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)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + } + [Fact] public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { @@ -59,8 +125,8 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:http:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -82,8 +148,31 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); 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(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.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // 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)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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.All(initialResult.EndPoints, ep => { @@ -101,10 +190,12 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", - ["services:basket:1"] = "http://remotehost:9090", - ["services:basket:2"] = "http://_grpc.localhost:2222", - ["services:basket:3"] = "grpc://remotehost:2222", + ["services:basket:http:0"] = "http://localhost:8080", + ["services:basket:https:1"] = "https://remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -125,9 +216,60 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(ResolutionStatus.Success, initialResult.Status); - Assert.Equal(2, initialResult.EndPoints.Count); + Assert.Equal(3, initialResult.EndPoints.Count); Assert.Equal(new DnsEndPoint("localhost", 2222), initialResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 2222), initialResult.EndPoints[1].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.All(initialResult.EndPoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.Null(hostNameFeature); + }); + } + } + + [Fact] + public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + { + var configSource = new MemoryConfigurationSource + { + InitialData = new Dictionary + { + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "remotehost:9090", + ["services:basket:grpc:0"] = "localhost:2222", + ["services:basket:grpc:1"] = "127.0.0.1:3333", + ["services:basket:grpc:2"] = "http://remotehost:4444", + ["services:basket:grpc:3"] = "https://remotehost:5555", + } + }; + var config = new ConfigurationBuilder().Add(configSource); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndPointResolver() + .BuildServiceProvider(); + var resolverFactory = services.GetRequiredService(); + ServiceEndPointWatcher resolver; + await using ((resolver = resolverFactory.CreateResolver("https+http://_grpc.basket")).ConfigureAwait(false)) + { + Assert.NotNull(resolver); + var tcs = new TaskCompletionSource(); + resolver.OnEndPointsUpdated = tcs.SetResult; + resolver.Start(); + 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); + + // 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); + + // 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.All(initialResult.EndPoints, ep => { diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index 696061949d9..d8adcbca529 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs @@ -49,7 +49,7 @@ public async Task ResolveServiceEndPoint_Superseded() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:http:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -72,7 +72,7 @@ public async Task ResolveServiceEndPoint_Superseded() // We expect the basket service to be resolved from Configuration, not the pass-through provider. Assert.Single(initialResult.EndPoints); - Assert.Equal(new DnsEndPoint("localhost", 8080), initialResult.EndPoints[0].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPoints[0].EndPoint); } } @@ -83,7 +83,7 @@ public async Task ResolveServiceEndPoint_Fallback() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -118,7 +118,7 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() { InitialData = new Dictionary { - ["services:basket:0"] = "http://localhost:8080", + ["services:basket:default:0"] = "http://localhost:8080", } }; var config = new ConfigurationBuilder().Add(configSource); From b982636b25c3a6dfb498f4b5fce1ba12a8bd535d Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 18 Mar 2024 17:29:22 +1100 Subject: [PATCH 31/77] Filling in empty doc comments. (#2968) * Docs! * Build failures. * Update src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.AWS/SDKResourceExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting.Azure/Extensions/AzureStorageExtensions.cs Co-authored-by: James Newton-King * Update src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs Co-authored-by: James Newton-King * A cancellation token instead of the cancellation token * More cancellation token. * Update src/Aspire.Hosting.AWS/CloudFormation/CloudFormationStackExecutor.cs Co-authored-by: James Newton-King --------- Co-authored-by: James Newton-King --- .../Http/HttpServiceEndPointResolver.cs | 2 +- .../ServiceEndPointResolver.cs | 2 +- .../ServiceEndPointWatcher.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index a135a2b02ed..b4f6249f28f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs @@ -26,7 +26,7 @@ public class HttpServiceEndPointResolver(ServiceEndPointResolverFactory resolver /// Resolves and returns a service endpoint for the specified request. /// /// The request message. - /// The cancellation token. + /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 965363dea9e..9de4a61b41b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs @@ -38,7 +38,7 @@ internal ServiceEndPointResolver(ServiceEndPointResolverFactory resolverProvider /// Resolves and returns service endpoints for the specified service. /// /// The service name. - /// The cancellation token. + /// A . /// The resolved service endpoints. public async ValueTask GetEndPointsAsync(string serviceName, CancellationToken cancellationToken) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 51e597ee89d..8936d3722b5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs @@ -57,7 +57,7 @@ public void Start() /// /// Returns a collection of resolved endpoints for the service. /// - /// The cancellation token. + /// A . /// A collection of resolved endpoints for the service. public ValueTask GetEndPointsAsync(CancellationToken cancellationToken = default) { From 4e104b0ca3259f1960197922e6278af6eba6057a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 18 Mar 2024 10:32:00 -0500 Subject: [PATCH 32/77] Remove ValueStopwatch (#2935) It is not necessary on net8.0. --- .../Http/ResolvingHttpClientHandler.cs | 6 +++--- .../Http/ResolvingHttpDelegatingHandler.cs | 6 +++--- .../Microsoft.Extensions.ServiceDiscovery.csproj | 1 - 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index ba32139f6ab..39eb65cc182 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.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. -using Microsoft.Extensions.Internal; +using System.Diagnostics; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -21,7 +21,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = ValueStopwatch.StartNew(); + var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -41,7 +41,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 2ab3c8dc3ec..976bfb331ed 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -1,8 +1,8 @@ // 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.Internal; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Abstractions; @@ -45,7 +45,7 @@ protected override async Task SendAsync(HttpRequestMessage var originalUri = request.RequestUri; IEndPointHealthFeature? epHealth = null; Exception? error = null; - var responseDuration = ValueStopwatch.StartNew(); + var startTime = Stopwatch.GetTimestamp(); if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); @@ -65,7 +65,7 @@ protected override async Task SendAsync(HttpRequestMessage } finally { - epHealth?.ReportHealth(responseDuration.GetElapsedTime(), error); // Report health so that the resolver pipeline can take health and performance into consideration, possibly triggering a circuit breaker?. + 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/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 60ec55fe9df..6836e58cf6b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,6 @@ - From a846b38fa3433f3e7781d37ecb2cf6f07236e2b8 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:54:38 -0700 Subject: [PATCH 33/77] Point to Kubernetes C# client logic from IsInKubernetesCluster() (#3049) --- .../DnsSrvServiceEndPointResolverProvider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index bf2f502c97a..d02dcfb7274 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs @@ -122,6 +122,8 @@ public bool TryCreateResolver(string serviceName, [NotNullWhen(true)] out IServi private static bool IsInKubernetesCluster() { + // This logic is based on the Kubernetes C# client logic found here: + // https://github.com/kubernetes-client/csharp/blob/52c3c00d4c55b28bdb491a219f4967823a83df2d/src/KubernetesClient/KubernetesClientConfiguration.InCluster.cs#L21 var host = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_HOST"); var port = Environment.GetEnvironmentVariable("KUBERNETES_SERVICE_PORT"); if (string.IsNullOrEmpty(host) || string.IsNullOrEmpty(port)) From 38756b14164276c1a88239a8a932a68ce3956ae7 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 26 Mar 2024 14:51:17 -0700 Subject: [PATCH 34/77] Service Discovery API refactoring (#3114) * API review feedback & general cleanup including removal of currently unused features * Align namespaces * Hide more of the API, rename for consistency * Hide more, rename more * ResolutionStatus does not need to be equatable * Make ServiceEndPointQuery public to break InternalsVisibleTo with Dns provider * Break InternalsVisibleTo from ServiceDiscovery package to YARP by adding a middleware factory * Remove ResolutionStatus, simplifying Service Discovery interfaces * Clean up ServiceEndPointImpl * Mark ServiceEndPointResolverResult as internal * Remove unnecessary members from ServiceEndPointCollection/Source * Seal service discovery types * Remove IServiceEndPointSelectorFactory and use DI instead * Remove unused endpoint selectors * Remove unused PendingStatusRefreshPeriod option * Rename UseServiceDiscovery to AddServiceDiscovery * Remove possible ambiguity in AddConfigurationServiceEndPointResolver signature * Add configuration delegate overloads to AddServiceDiscovery methods * Clean up logging in configuration-based service endpoint provider * API review: rename ServiceEndPointCollectionSource to IServiceEndPointBuilder * Rename IServiceDiscoveryDelegatingHttpMessageHandlerFactory * Rename IServiceEndPointProvider.ResolveAsync to PopulateAsync * Hide IServiceEndPointSelector * Remove allowedSchemes from ServiceEndPointQuery.TryParse * Rename ServiceEndPointQuery.Host to .ServiceName * Fix build * Review feedback * nit param rename * Improve ServiceEndPointQuery.ToString output * fixup --- .../Features/IEndPointHealthFeature.cs | 18 -- .../Features/IEndPointLoadFeature.cs | 16 -- .../{Features => }/IHostNameFeature.cs | 4 +- .../IServiceEndPointBuilder.cs | 29 +++ .../IServiceEndPointProvider.cs | 4 +- ....cs => IServiceEndPointProviderFactory.cs} | 10 +- .../IServiceEndPointSelectorProvider.cs | 16 -- .../Internal/ServiceEndPointImpl.cs | 18 +- .../ResolutionStatus.cs | 101 --------- .../ResolutionStatusCode.cs | 40 ---- .../ServiceEndPoint.cs | 3 +- .../ServiceEndPointCollectionSource.cs | 57 ----- .../ServiceEndPointQuery.cs | 97 +++++++++ .../ServiceEndPointResolverResult.cs | 30 --- ...Collection.cs => ServiceEndPointSource.cs} | 36 +--- .../DnsServiceEndPointResolver.cs | 4 +- .../DnsServiceEndPointResolverBase.cs | 68 ++---- .../DnsServiceEndPointResolverOptions.cs | 2 - .../DnsServiceEndPointResolverProvider.cs | 16 +- .../DnsSrvServiceEndPointResolver.cs | 4 +- .../DnsSrvServiceEndPointResolverOptions.cs | 2 - .../DnsSrvServiceEndPointResolverProvider.cs | 21 +- ...iscoveryDnsServiceCollectionExtensions.cs} | 8 +- .../ServiceDiscoveryDestinationResolver.cs | 22 +- ...viceDiscoveryForwarderHttpClientFactory.cs | 12 +- ...everseProxyServiceCollectionExtensions.cs} | 3 +- ...onfigurationServiceEndPointResolver.Log.cs | 42 +--- .../ConfigurationServiceEndPointResolver.cs | 79 +++---- ...erviceEndPointResolverOptionsValidator.cs} | 20 +- ...gurationServiceEndPointResolverProvider.cs | 13 +- ...igurationServiceEndPointResolverOptions.cs | 22 ++ .../Http/HttpServiceEndPointResolver.cs | 14 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Http/ResolvingHttpClientHandler.cs | 27 +-- .../Http/ResolvingHttpDelegatingHandler.cs | 16 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Internal/ServiceEndPointResolverResult.cs | 30 +++ .../Internal/ServiceNameParser.cs | 78 ------- .../Internal/ServiceNameParts.cs | 39 ---- .../IServiceEndPointSelector.cs | 8 +- .../PickFirstServiceEndPointSelector.cs | 29 --- ...ickFirstServiceEndPointSelectorProvider.cs | 18 -- ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 ----- ...oChoicesServiceEndPointSelectorProvider.cs | 18 -- .../RandomServiceEndPointSelector.cs | 29 --- .../RandomServiceEndPointSelectorProvider.cs | 18 -- .../RoundRobinServiceEndPointSelector.cs | 10 +- ...undRobinServiceEndPointSelectorProvider.cs | 18 -- ...crosoft.Extensions.ServiceDiscovery.csproj | 3 +- .../PassThroughServiceEndPointResolver.cs | 16 +- ...sThroughServiceEndPointResolverProvider.cs | 6 +- ...ceDiscoveryHttpClientBuilderExtensions.cs} | 33 +-- .../ServiceDiscoveryOptions.cs | 46 +++- ...ceDiscoveryServiceCollectionExtensions.cs} | 53 +++-- .../ServiceEndPointBuilder.cs | 46 ++++ .../ServiceEndPointResolver.cs | 11 +- .../ServiceEndPointResolverOptions.cs | 22 -- .../ServiceEndPointWatcher.Log.cs | 5 +- .../ServiceEndPointWatcher.cs | 199 ++++-------------- ...s => ServiceEndPointWatcherFactory.Log.cs} | 3 +- ...ry.cs => ServiceEndPointWatcherFactory.cs} | 22 +- .../UriEndPoint.cs | 4 +- .../DnsSrvServiceEndPointResolverTests.cs | 40 ++-- ...nfigurationServiceEndPointResolverTests.cs | 88 ++++---- ...PassThroughServiceEndPointResolverTests.cs | 34 ++- .../ServiceEndPointResolverTests.cs | 76 ++++--- 66 files changed, 680 insertions(+), 1284 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{Features => }/IHostNameFeature.cs (70%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolverProvider.cs => IServiceEndPointProviderFactory.cs} (59%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointCollection.cs => ServiceEndPointSource.cs} (55%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{HostingExtensions.cs => ServiceDiscoveryDnsServiceCollectionExtensions.cs} (88%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/{ReverseProxyServiceCollectionExtensions.cs => ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs} (94%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndPointResolverOptionsValidator.cs} (50%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery/LoadBalancing}/IServiceEndPointSelector.cs (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Http/HttpClientBuilderExtensions.cs => ServiceDiscoveryHttpClientBuilderExtensions.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{HostingExtensions.cs => ServiceDiscoveryServiceCollectionExtensions.cs} (57%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.Log.cs => ServiceEndPointWatcherFactory.Log.cs} (90%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.cs => ServiceEndPointWatcherFactory.cs} (69%) rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery}/UriEndPoint.cs (86%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs deleted file mode 100644 index 63dc3e11a3a..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs deleted file mode 100644 index 2610f135945..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs index fff3c3fa3f8..c7489472374 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs new file mode 100644 index 00000000000..468adea1c09 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index 3b369a97850..950823257af 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs similarity index 59% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs index 51343a53697..4b1876f808e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs deleted file mode 100644 index 27f4ec4324a..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index b73635ecd57..7d135dfe97d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs deleted file mode 100644 index 04eec95dc63..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs deleted file mode 100644 index 7157eac758f..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs index 9dc4675dade..a3cde62ce0d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs deleted file mode 100644 index 94f274a38e8..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs new file mode 100644 index 00000000000..99c92cce27c --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs deleted file mode 100644 index 9179ed2f113..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs similarity index 55% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs index c9540f2e7a1..807981226e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4a8350483eb..a2601c84b45 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 516c8ed1f69..9d6c54e4755 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 37879b5f3e3..665c98bbc09 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 8f676327d5f..51525663a03 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 97a0d47d028..d59dbfbb69c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 5bac96c6c0a..704e03cd9ca 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index d02dcfb7274..8a75c1d1bbf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs similarity index 88% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index e385bde69a7..0d795660fd2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index e35be5d629b..22d7e6d8327 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index d37e4f1407d..84aafe2a67e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index e52ff65ee2a..9f473fd3a9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index 5916951c69d..fdb61ef59f4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index dae054c9883..9604ec0c201 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs index c83589eb268..91e97b5d0bc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 472205f12f9..032c50b6f27 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..d3b94f2f1e7 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index b4f6249f28f..44e58b0dbbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0febfa94815 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 39eb65cc182..bc06a031700 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 976bfb331ed..daa7b8a17de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0d3ba00122f --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..07bffa5654b --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs deleted file mode 100644 index de047481872..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs deleted file mode 100644 index f93729a40ec..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs index e2ffbd0421f..bd0172c45cf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs deleted file mode 100644 index 9395896e520..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs deleted file mode 100644 index d3f657c9550..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs deleted file mode 100644 index e233dfb7b5c..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs deleted file mode 100644 index 00832bc7811..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs deleted file mode 100644 index 8e4bb2378d8..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs deleted file mode 100644 index ae74b4032bc..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs index 5848c7d8f72..92da7cf25bf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs deleted file mode 100644 index 40d9ce7845c..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6836e58cf6b..9a5d67db04e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index ab0ea286b68..483c08702df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index b3a326010bb..83455e0979c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index c1c833de89f..bcfa59056cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index d9510a3cf22..89c5a2d2eb0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs similarity index 57% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a4ba9b63b31..6403b214631 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs new file mode 100644 index 00000000000..1a14cb961b7 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 9de4a61b41b..029d2601243 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs deleted file mode 100644 index 415a2192c30..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index 17864811062..78a8f84b556 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 8936d3722b5..9b1069d31e7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs similarity index 90% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs index d7835f26d08..69f565eb8e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs index c545c82e9e6..90f62ab0597 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs index 6d3132da880..6b5b07d199e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs +++ b/src/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7e2ef478ef7..25cd88a1436 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index f35ffa2026c..6d8091f026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index d8adcbca529..643bbfad441 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 0628bedbe73..f5e506a9b72 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/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); From 7e294b992a3a4526319c8b7b228f960ae09122b5 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 26 Mar 2024 22:24:30 -0700 Subject: [PATCH 35/77] Service Discovery API refactoring (#3114) (#3196) * API review feedback & general cleanup including removal of currently unused features * Align namespaces * Hide more of the API, rename for consistency * Hide more, rename more * ResolutionStatus does not need to be equatable * Make ServiceEndPointQuery public to break InternalsVisibleTo with Dns provider * Break InternalsVisibleTo from ServiceDiscovery package to YARP by adding a middleware factory * Remove ResolutionStatus, simplifying Service Discovery interfaces * Clean up ServiceEndPointImpl * Mark ServiceEndPointResolverResult as internal * Remove unnecessary members from ServiceEndPointCollection/Source * Seal service discovery types * Remove IServiceEndPointSelectorFactory and use DI instead * Remove unused endpoint selectors * Remove unused PendingStatusRefreshPeriod option * Rename UseServiceDiscovery to AddServiceDiscovery * Remove possible ambiguity in AddConfigurationServiceEndPointResolver signature * Add configuration delegate overloads to AddServiceDiscovery methods * Clean up logging in configuration-based service endpoint provider * API review: rename ServiceEndPointCollectionSource to IServiceEndPointBuilder * Rename IServiceDiscoveryDelegatingHttpMessageHandlerFactory * Rename IServiceEndPointProvider.ResolveAsync to PopulateAsync * Hide IServiceEndPointSelector * Remove allowedSchemes from ServiceEndPointQuery.TryParse * Rename ServiceEndPointQuery.Host to .ServiceName * Fix build * Review feedback * nit param rename * Improve ServiceEndPointQuery.ToString output * fixup (cherry picked from commit 38756b14164276c1a88239a8a932a68ce3956ae7) --- .../Features/IEndPointHealthFeature.cs | 18 -- .../Features/IEndPointLoadFeature.cs | 16 -- .../{Features => }/IHostNameFeature.cs | 4 +- .../IServiceEndPointBuilder.cs | 29 +++ .../IServiceEndPointProvider.cs | 4 +- ....cs => IServiceEndPointProviderFactory.cs} | 10 +- .../IServiceEndPointSelectorProvider.cs | 16 -- .../Internal/ServiceEndPointImpl.cs | 18 +- .../ResolutionStatus.cs | 101 --------- .../ResolutionStatusCode.cs | 40 ---- .../ServiceEndPoint.cs | 3 +- .../ServiceEndPointCollectionSource.cs | 57 ----- .../ServiceEndPointQuery.cs | 97 +++++++++ .../ServiceEndPointResolverResult.cs | 30 --- ...Collection.cs => ServiceEndPointSource.cs} | 36 +--- .../DnsServiceEndPointResolver.cs | 4 +- .../DnsServiceEndPointResolverBase.cs | 68 ++---- .../DnsServiceEndPointResolverOptions.cs | 2 - .../DnsServiceEndPointResolverProvider.cs | 16 +- .../DnsSrvServiceEndPointResolver.cs | 4 +- .../DnsSrvServiceEndPointResolverOptions.cs | 2 - .../DnsSrvServiceEndPointResolverProvider.cs | 21 +- ...iscoveryDnsServiceCollectionExtensions.cs} | 8 +- .../ServiceDiscoveryDestinationResolver.cs | 22 +- ...viceDiscoveryForwarderHttpClientFactory.cs | 12 +- ...everseProxyServiceCollectionExtensions.cs} | 3 +- ...onfigurationServiceEndPointResolver.Log.cs | 42 +--- .../ConfigurationServiceEndPointResolver.cs | 79 +++---- ...erviceEndPointResolverOptionsValidator.cs} | 20 +- ...gurationServiceEndPointResolverProvider.cs | 13 +- ...igurationServiceEndPointResolverOptions.cs | 22 ++ .../Http/HttpServiceEndPointResolver.cs | 14 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Http/ResolvingHttpClientHandler.cs | 27 +-- .../Http/ResolvingHttpDelegatingHandler.cs | 16 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 19 ++ .../Internal/ServiceEndPointResolverResult.cs | 30 +++ .../Internal/ServiceNameParser.cs | 78 ------- .../Internal/ServiceNameParts.cs | 39 ---- .../IServiceEndPointSelector.cs | 8 +- .../PickFirstServiceEndPointSelector.cs | 29 --- ...ickFirstServiceEndPointSelectorProvider.cs | 18 -- ...owerOfTwoChoicesServiceEndPointSelector.cs | 50 ----- ...oChoicesServiceEndPointSelectorProvider.cs | 18 -- .../RandomServiceEndPointSelector.cs | 29 --- .../RandomServiceEndPointSelectorProvider.cs | 18 -- .../RoundRobinServiceEndPointSelector.cs | 10 +- ...undRobinServiceEndPointSelectorProvider.cs | 18 -- ...crosoft.Extensions.ServiceDiscovery.csproj | 3 +- .../PassThroughServiceEndPointResolver.cs | 16 +- ...sThroughServiceEndPointResolverProvider.cs | 6 +- ...ceDiscoveryHttpClientBuilderExtensions.cs} | 33 +-- .../ServiceDiscoveryOptions.cs | 46 +++- ...ceDiscoveryServiceCollectionExtensions.cs} | 53 +++-- .../ServiceEndPointBuilder.cs | 46 ++++ .../ServiceEndPointResolver.cs | 11 +- .../ServiceEndPointResolverOptions.cs | 22 -- .../ServiceEndPointWatcher.Log.cs | 5 +- .../ServiceEndPointWatcher.cs | 199 ++++-------------- ...s => ServiceEndPointWatcherFactory.Log.cs} | 3 +- ...ry.cs => ServiceEndPointWatcherFactory.cs} | 22 +- .../UriEndPoint.cs | 4 +- .../DnsSrvServiceEndPointResolverTests.cs | 40 ++-- ...nfigurationServiceEndPointResolverTests.cs | 88 ++++---- ...PassThroughServiceEndPointResolverTests.cs | 34 ++- .../ServiceEndPointResolverTests.cs | 76 ++++--- 66 files changed, 680 insertions(+), 1284 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{Features => }/IHostNameFeature.cs (70%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointResolverProvider.cs => IServiceEndPointProviderFactory.cs} (59%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointCollection.cs => ServiceEndPointSource.cs} (55%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{HostingExtensions.cs => ServiceDiscoveryDnsServiceCollectionExtensions.cs} (88%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/{ReverseProxyServiceCollectionExtensions.cs => ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs} (94%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndPointResolverOptionsValidator.cs} (50%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery/LoadBalancing}/IServiceEndPointSelector.cs (73%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{Http/HttpClientBuilderExtensions.cs => ServiceDiscoveryHttpClientBuilderExtensions.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{HostingExtensions.cs => ServiceDiscoveryServiceCollectionExtensions.cs} (57%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.Log.cs => ServiceEndPointWatcherFactory.Log.cs} (90%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolverFactory.cs => ServiceEndPointWatcherFactory.cs} (69%) rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery.Abstractions => Microsoft.Extensions.ServiceDiscovery}/UriEndPoint.cs (86%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointHealthFeature.cs deleted file mode 100644 index 63dc3e11a3a..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IEndPointLoadFeature.cs deleted file mode 100644 index 2610f135945..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IHostNameFeature.cs index fff3c3fa3f8..c7489472374 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Features/IHostNameFeature.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs new file mode 100644 index 00000000000..468adea1c09 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs index 3b369a97850..950823257af 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs similarity index 59% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs index 51343a53697..4b1876f808e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelectorProvider.cs deleted file mode 100644 index 27f4ec4324a..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs index b73635ecd57..7d135dfe97d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatus.cs deleted file mode 100644 index 04eec95dc63..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ResolutionStatusCode.cs deleted file mode 100644 index 7157eac758f..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs index 9dc4675dade..a3cde62ce0d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollectionSource.cs deleted file mode 100644 index 94f274a38e8..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs new file mode 100644 index 00000000000..99c92cce27c --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointResolverResult.cs deleted file mode 100644 index 9179ed2f113..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs similarity index 55% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs index c9540f2e7a1..807981226e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointCollection.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs index 4a8350483eb..a2601c84b45 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs index 516c8ed1f69..9d6c54e4755 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs index 37879b5f3e3..665c98bbc09 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs index 8f676327d5f..51525663a03 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs index 97a0d47d028..d59dbfbb69c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs index 5bac96c6c0a..704e03cd9ca 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs index d02dcfb7274..8a75c1d1bbf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs similarity index 88% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index e385bde69a7..0d795660fd2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/HostingExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index e35be5d629b..22d7e6d8327 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs index d37e4f1407d..84aafe2a67e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryForwarderHttpClientFactory.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs similarity index 94% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index e52ff65ee2a..9f473fd3a9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs index 5916951c69d..fdb61ef59f4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs index dae054c9883..9604ec0c201 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs similarity index 50% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs index c83589eb268..91e97b5d0bc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs index 472205f12f9..032c50b6f27 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs new file mode 100644 index 00000000000..d3b94f2f1e7 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs index b4f6249f28f..44e58b0dbbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0febfa94815 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index 39eb65cc182..bc06a031700 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index 976bfb331ed..daa7b8a17de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs new file mode 100644 index 00000000000..0d3ba00122f --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs new file mode 100644 index 00000000000..07bffa5654b --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParser.cs deleted file mode 100644 index de047481872..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceNameParts.cs deleted file mode 100644 index f93729a40ec..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs index e2ffbd0421f..bd0172c45cf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointSelector.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelector.cs deleted file mode 100644 index 9395896e520..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PickFirstServiceEndPointSelectorProvider.cs deleted file mode 100644 index d3f657c9550..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelector.cs deleted file mode 100644 index e233dfb7b5c..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/PowerOfTwoChoicesServiceEndPointSelectorProvider.cs deleted file mode 100644 index 00832bc7811..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelector.cs deleted file mode 100644 index 8e4bb2378d8..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RandomServiceEndPointSelectorProvider.cs deleted file mode 100644 index ae74b4032bc..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs index 5848c7d8f72..92da7cf25bf 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelectorProvider.cs deleted file mode 100644 index 40d9ce7845c..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 6836e58cf6b..9a5d67db04e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs index ab0ea286b68..483c08702df 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs index b3a326010bb..83455e0979c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index c1c833de89f..bcfa59056cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpClientBuilderExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index d9510a3cf22..89c5a2d2eb0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs similarity index 57% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a4ba9b63b31..6403b214631 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/HostingExtensions.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs new file mode 100644 index 00000000000..1a14cb961b7 --- /dev/null +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs index 9de4a61b41b..029d2601243 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverOptions.cs deleted file mode 100644 index 415a2192c30..00000000000 --- a/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs index 17864811062..78a8f84b556 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs index 8936d3722b5..9b1069d31e7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs similarity index 90% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs index d7835f26d08..69f565eb8e3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.Log.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs index c545c82e9e6..90f62ab0597 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolverFactory.cs +++ b/src/Libraries/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/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs index 6d3132da880..6b5b07d199e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs +++ b/src/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs index 7e2ef478ef7..25cd88a1436 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs index f35ffa2026c..6d8091f026c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs index d8adcbca529..643bbfad441 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/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/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs index 0628bedbe73..f5e506a9b72 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/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); From 76c92cbc99bffcf71016a406630febe678753bcd Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 2 Apr 2024 18:38:23 +1100 Subject: [PATCH 36/77] Remove obsolete APIs. (#3329) --- .../ServiceDiscoveryHttpClientBuilderExtensions.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index bcfa59056cc..b4c34ccb7c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -14,14 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceDiscoveryHttpClientBuilderExtensions { - /// - /// Adds service discovery to the . - /// - /// The builder. - /// The builder. - [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 . /// From 2837a066d733d9e9773f6ccd5391a632640c73ce Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:15:52 -0500 Subject: [PATCH 37/77] Remove obsolete APIs. (#3336) Co-authored-by: Mitch Denny --- .../ServiceDiscoveryHttpClientBuilderExtensions.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index bcfa59056cc..b4c34ccb7c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -14,14 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class ServiceDiscoveryHttpClientBuilderExtensions { - /// - /// Adds service discovery to the . - /// - /// The builder. - /// The builder. - [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 . /// From 30697f6b7e9134bf3db6cf60a78cd98d6e8caae1 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:34:43 -0700 Subject: [PATCH 38/77] Service Discovery: Implement approved API (#3413) * Rename EndPoint to Endpoint, resolver to provider * Apply changes decided during API review * Find & fix straggler file names * Variables and members assignable to System.Net.EndPoint use upper-case P * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs Co-authored-by: Stephen Halter * Delete GetEndpointString() --------- Co-authored-by: Stephen Halter --- .../IServiceEndPointProviderFactory.cs | 20 -- ...tBuilder.cs => IServiceEndpointBuilder.cs} | 8 +- ...rovider.cs => IServiceEndpointProvider.cs} | 6 +- .../IServiceEndpointProviderFactory.cs | 20 ++ ...EndPointImpl.cs => ServiceEndpointImpl.cs} | 8 +- .../README.md | 2 +- ...{ServiceEndPoint.cs => ServiceEndpoint.cs} | 21 +-- ...dPointQuery.cs => ServiceEndpointQuery.cs} | 35 ++-- ...ointSource.cs => ServiceEndpointSource.cs} | 16 +- .../DnsServiceEndPointResolverProvider.cs | 21 --- ...olver.cs => DnsServiceEndpointProvider.cs} | 28 +-- ... => DnsServiceEndpointProviderBase.Log.cs} | 2 +- ...e.cs => DnsServiceEndpointProviderBase.cs} | 36 ++-- .../DnsServiceEndpointProviderFactory.cs | 21 +++ ...s => DnsServiceEndpointProviderOptions.cs} | 6 +- ...er.cs => DnsSrvServiceEndpointProvider.cs} | 34 ++-- ...> DnsSrvServiceEndpointProviderFactory.cs} | 19 +- ...> DnsSrvServiceEndpointProviderOptions.cs} | 6 +- .../README.md | 16 +- ...DiscoveryDnsServiceCollectionExtensions.cs | 33 +++- .../ServiceDiscoveryDestinationResolver.cs | 10 +- ...nfigurationServiceEndpointProvider.Log.cs} | 12 +- ...> ConfigurationServiceEndpointProvider.cs} | 64 +++---- ...gurationServiceEndpointProviderFactory.cs} | 12 +- ...erviceEndpointProviderOptionsValidator.cs} | 10 +- ...gurationServiceEndpointProviderOptions.cs} | 6 +- ...lver.cs => HttpServiceEndpointResolver.cs} | 46 ++--- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 2 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 20 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 4 +- ...lt.cs => ServiceEndpointResolverResult.cs} | 8 +- ...elector.cs => IServiceEndpointSelector.cs} | 10 +- ...s => RoundRobinServiceEndpointSelector.cs} | 12 +- ...PassThroughServiceEndpointProvider.Log.cs} | 4 +- ... => PassThroughServiceEndpointProvider.cs} | 14 +- ...sThroughServiceEndpointProviderFactory.cs} | 20 +- .../README.md | 76 ++++---- ...iceDiscoveryHttpClientBuilderExtensions.cs | 4 +- .../ServiceDiscoveryOptions.cs | 23 +-- ...iceDiscoveryServiceCollectionExtensions.cs | 40 ++-- .../ServiceEndPointWatcherFactory.cs | 61 ------ ...ntBuilder.cs => ServiceEndpointBuilder.cs} | 12 +- ...Resolver.cs => ServiceEndpointResolver.cs} | 36 ++-- ...r.Log.cs => ServiceEndpointWatcher.Log.cs} | 24 +-- ...ntWatcher.cs => ServiceEndpointWatcher.cs} | 80 ++++---- ...s => ServiceEndpointWatcherFactory.Log.cs} | 11 +- .../ServiceEndpointWatcherFactory.cs | 61 ++++++ ... => DnsSrvServiceEndpointResolverTests.cs} | 125 ++++-------- ...figurationServiceEndpointResolverTests.cs} | 178 +++++++++--------- ...assThroughServiceEndpointResolverTests.cs} | 74 ++++---- ...sts.cs => ServiceEndpointResolverTests.cs} | 150 +++++++-------- 52 files changed, 763 insertions(+), 810 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointBuilder.cs => IServiceEndpointBuilder.cs} (80%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointProvider.cs => IServiceEndpointProvider.cs} (75%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/{ServiceEndPointImpl.cs => ServiceEndpointImpl.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPoint.cs => ServiceEndpoint.cs} (52%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointQuery.cs => ServiceEndpointQuery.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointSource.cs => ServiceEndpointSource.cs} (74%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.cs => DnsServiceEndpointProvider.cs} (61%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.Log.cs => DnsServiceEndpointProviderBase.Log.cs} (97%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.cs => DnsServiceEndpointProviderBase.cs} (81%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverOptions.cs => DnsServiceEndpointProviderOptions.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolver.cs => DnsSrvServiceEndpointProvider.cs} (71%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverProvider.cs => DnsSrvServiceEndpointProviderFactory.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverOptions.cs => DnsSrvServiceEndpointProviderOptions.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.Log.cs => ConfigurationServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.cs => ConfigurationServiceEndpointProvider.cs} (76%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverProvider.cs => ConfigurationServiceEndpointProviderFactory.cs} (58%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptionsValidator.cs => ConfigurationServiceEndpointProviderOptionsValidator.cs} (62%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndpointProviderOptions.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/{HttpServiceEndPointResolver.cs => HttpServiceEndpointResolver.cs} (82%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/{ServiceEndPointResolverResult.cs => ServiceEndpointResolverResult.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{IServiceEndPointSelector.cs => IServiceEndpointSelector.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{RoundRobinServiceEndPointSelector.cs => RoundRobinServiceEndpointSelector.cs} (63%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.Log.cs => PassThroughServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.cs => PassThroughServiceEndpointProvider.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolverProvider.cs => PassThroughServiceEndpointProviderFactory.cs} (70%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointBuilder.cs => ServiceEndpointBuilder.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.cs => ServiceEndpointResolver.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.Log.cs => ServiceEndpointWatcher.Log.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.cs => ServiceEndpointWatcher.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcherFactory.Log.cs => ServiceEndpointWatcherFactory.Log.cs} (53%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsSrvServiceEndPointResolverTests.cs => DnsSrvServiceEndpointResolverTests.cs} (72%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ConfigurationServiceEndPointResolverTests.cs => ConfigurationServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{PassThroughServiceEndPointResolverTests.cs => PassThroughServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ServiceEndPointResolverTests.cs => ServiceEndpointResolverTests.cs} (62%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs deleted file mode 100644 index 4b1876f808e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs +++ /dev/null @@ -1,20 +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; - -/// -/// Creates instances. -/// -public interface IServiceEndPointProviderFactory -{ - /// - /// Tries to create an instance for the specified . - /// - /// The service to create the resolver for. - /// The resolver. - /// if the resolver was created, otherwise. - bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs similarity index 80% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs index 468adea1c09..e051b2bf746 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs @@ -7,14 +7,14 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Builder to create a instances. +/// Builder to create a instances. /// -public interface IServiceEndPointBuilder +public interface IServiceEndpointBuilder { /// /// Gets the endpoints. /// - IList EndPoints { get; } + IList Endpoints { get; } /// /// Gets the feature collection. @@ -22,7 +22,7 @@ public interface IServiceEndPointBuilder IFeatureCollection Features { get; } /// - /// Adds a change token to the resulting . + /// Adds a change token to the resulting . /// /// The change token. void AddChangeToken(IChangeToken changeToken); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs index 950823257af..4a192180b66 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. /// -public interface IServiceEndPointProvider : IAsyncDisposable +public interface IServiceEndpointProvider : IAsyncDisposable { /// /// Resolves the endpoints for the service. /// - /// The endpoint collection, which resolved endpoints will be added to. + /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..009cbf05d76 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Creates instances. +/// +public interface IServiceEndpointProviderFactory +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the provider for. + /// The provider. + /// if the provider was created, otherwise. + bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 7d135dfe97d..8bfb50fe930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -6,9 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint +internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); - public override string? ToString() => GetEndPointString(); + public override string? ToString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + _ => EndPoint.ToString()! + }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md index c5cf6b9bc78..0d97211313e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -1,6 +1,6 @@ # Microsoft.Extensions.ServiceDiscovery.Abstractions -The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint providers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). ## Feedback & contributing diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs similarity index 52% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index a3cde62ce0d..238e383a957 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.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 System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -11,8 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. /// -[DebuggerDisplay("{GetEndPointString(),nq}")] -public abstract class ServiceEndPoint +public abstract class ServiceEndpoint { /// /// Gets the endpoint. @@ -25,21 +23,10 @@ public abstract class ServiceEndPoint public abstract IFeatureCollection Features { get; } /// - /// Creates a new . + /// Creates a new . /// /// The endpoint being represented. /// Features of the endpoint. - /// A newly initialized . - public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); - - /// - /// Gets a string representation of the . - /// - /// A string representation of the . - public virtual string GetEndPointString() => EndPoint switch - { - DnsEndPoint dns => $"{dns.Host}:{dns.Port}", - IPEndPoint ip => ip.ToString(), - _ => EndPoint.ToString()! - }; + /// A newly initialized . + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 99c92cce27c..600dc5cc28c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -8,21 +8,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Describes a query for endpoints of a service. /// -public sealed class ServiceEndPointQuery +public sealed class ServiceEndpointQuery { + private readonly string _originalString; + /// - /// Initializes a new instance. + /// 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) + /// The optional endpoint name. + private ServiceEndpointQuery(string originalString, string[] includedSchemes, string serviceName, string? endpointName) { - OriginalString = originalString; - IncludeSchemes = includedSchemes; + _originalString = originalString; + IncludedSchemes = includedSchemes; ServiceName = serviceName; - EndPointName = endPointName; + EndpointName = endpointName; } /// @@ -31,7 +33,7 @@ private ServiceEndPointQuery(string originalString, string[] includedSchemes, st /// 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) + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) @@ -52,10 +54,10 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin var uriHost = uri.Host; var segmentSeparatorIndex = uriHost.IndexOf('.'); string host; - string? endPointName = null; + string? endpointName = null; if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') { - endPointName = uriHost[1..segmentSeparatorIndex]; + endpointName = uriHost[1..segmentSeparatorIndex]; // Skip the endpoint name, including its prefix ('_') and suffix ('.'). host = uriHost[(segmentSeparatorIndex + 1)..]; @@ -67,24 +69,19 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin // 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); + 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; } + public IReadOnlyList IncludedSchemes { get; } /// /// Gets the endpoint name, or if no endpoint name is specified. /// - public string? EndPointName { get; } + public string? EndpointName { get; } /// /// Gets the service name. @@ -92,6 +89,6 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin 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)}"; + public override string? ToString() => _originalString; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs similarity index 74% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index 807981226e3..fb5bff1b288 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -11,18 +11,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public sealed class ServiceEndPointSource +[DebuggerTypeProxy(typeof(ServiceEndpointCollectionDebuggerView))] +public sealed class ServiceEndpointSource { - private readonly List? _endpoints; + private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); @@ -34,7 +34,7 @@ public ServiceEndPointSource(List? endpoints, IChangeToken chan /// /// Gets the endpoints. /// - public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; + public IReadOnlyList Endpoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -57,13 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) + private sealed class ServiceEndpointCollectionDebuggerView(ServiceEndpointSource value) { public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); + public ServiceEndpoint[] Endpoints => value.Endpoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs deleted file mode 100644 index 51525663a03..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ /dev/null @@ -1,21 +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.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Dns; - -internal sealed partial class DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : IServiceEndPointProviderFactory -{ - /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) - { - resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); - return true; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index a2601c84b45..6cc9f92bc46 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -7,12 +7,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsServiceEndpointProvider( + ServiceEndpointQuery query, string hostName, - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; @@ -26,27 +26,27 @@ internal sealed partial class DnsServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - endPoints.Add(serviceEndPoint); + endpoints.Add(serviceEndpoint); } - if (endPoints.Count == 0) + if (endpoints.Count == 0) { - throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); + throw new InvalidOperationException($"No DNS records were found for service '{ServiceName}' (DNS name: '{hostName}')."); } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs index cd664215aa9..29aaaf8e930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndpointProviderBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 9d6c54e4755..6c69cc7a760 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -7,9 +7,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// A service end point resolver that uses DNS to resolve the service end points. +/// A service end point provider that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider +internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpointProvider { private readonly object _lock = new(); private readonly ILogger _logger; @@ -20,23 +20,23 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; + private List? _lastEndpointCollection; private TimeSpan _nextRefreshPeriod; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. + /// The service name. /// The logger. /// The time provider. - protected DnsServiceEndPointResolverBase( - string serviceName, + protected DnsServiceEndpointProviderBase( + ServiceEndpointQuery query, ILogger logger, TimeProvider timeProvider) { - ServiceName = serviceName; + ServiceName = query.ToString()!; _logger = logger; - _lastEndPointCollection = null; + _lastEndpointCollection = null; _timeProvider = timeProvider; _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); @@ -58,10 +58,10 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask PopulateAsync(IServiceEndPointBuilder 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) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return; @@ -85,28 +85,28 @@ public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, Cancella lock (_lock) { - if (_lastEndPointCollection is { Count: > 0 } eps) + if (_lastEndpointCollection is { Count: > 0 } eps) { foreach (var ep in eps) { - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } - endPoints.AddChangeToken(_lastChangeToken); + endpoints.AddChangeToken(_lastChangeToken); return; } } - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + private bool ShouldRefresh() => _lastEndpointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; protected abstract Task ResolveAsyncCore(); - protected void SetResult(List endPoints, TimeSpan validityPeriod) + protected void SetResult(List endpoints, TimeSpan validityPeriod) { lock (_lock) { - if (endPoints is { Count: > 0 }) + if (endpoints is { Count: > 0 }) { _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); _nextRefreshPeriod = DefaultRefreshPeriod; @@ -131,7 +131,7 @@ protected void SetResult(List endPoints, TimeSpan validityPerio _lastCollectionCancellation.Cancel(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; + _lastEndpointCollection = endpoints; } TimeSpan GetRefreshPeriod() diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..c241ad89dd3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -0,0 +1,21 @@ +// 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.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndpointProviderFactory +{ + /// + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) + { + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs index 665c98bbc09..b163afc76ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsServiceEndPointResolverOptions +public class DnsServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -31,5 +31,5 @@ public class DnsServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs similarity index 71% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index d59dbfbb69c..dd17a7e2732 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -9,14 +9,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsSrvServiceEndpointProvider( + ServiceEndpointQuery query, string srvQuery, string hostName, - IOptionsMonitor options, - ILogger logger, + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); @@ -59,15 +59,15 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); + endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) { @@ -79,22 +79,22 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) { var msg = errorMessage switch { - { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", - _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", + _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." }; return new InvalidOperationException(msg); } - ServiceEndPoint CreateEndPoint(EndPoint endPoint) + ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 8a75c1d1bbf..fd0cb28353d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -8,11 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, +internal sealed partial class DnsSrvServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointProviderFactory + 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"); @@ -20,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // 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 @@ -30,17 +30,16 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou // 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); - resolver = default; + DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); + provider = default; return false; } - var portName = query.EndPointName ?? "default"; + var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index 704e03cd9ca..c908c56d770 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsSrvServiceEndPointResolverOptions +public class DnsSrvServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -39,5 +39,5 @@ public class DnsSrvServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md index d3fbe2a75e5..8be4560870b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -1,25 +1,25 @@ # Microsoft.Extensions.ServiceDiscovery.Dns -This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint providers: -- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. - _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). ## Resolving service endpoints with DNS -The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +The _DNS_ service endpoint provider resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS service endpoint provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. -To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: +To configure the DNS service endpoint provider in your application, add the DNS service endpoint provider to your host builder's service collection using the `AddDnsServiceEndpointProvider` method. service discovery as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsServiceEndPointResolver(); +builder.Services.AddDnsServiceEndpointProvider(); ``` ## Resolving service endpoints in Kubernetes with DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -37,11 +37,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 0d795660fd2..17544d09486 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -14,6 +14,17 @@ namespace Microsoft.Extensions.Hosting; /// public static class ServiceDiscoveryDnsServiceCollectionExtensions { + /// + /// Adds DNS SRV service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + /// /// Adds DNS SRV service discovery to the . /// @@ -24,16 +35,26 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } + /// + /// Adds DNS service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + /// /// Adds DNS service discovery to the . /// @@ -43,11 +64,11 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 22d7e6d8327..8ec810f2c05 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,14 +54,14 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(result.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 result.EndPoints) + foreach (var endpoint in result.Endpoints) { - var addressString = endPoint.GetEndPointString(); + var addressString = endpoint.ToString()!; Uri uri; if (!addressString.Contains("://")) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index fdb61ef59f4..48c922a85b3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed partial class ConfigurationServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndpointProvider { private sealed partial class Log { @@ -19,10 +19,10 @@ private sealed partial class Log [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(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); + [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); - internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) + internal static void ConfiguredEndpoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -40,8 +40,8 @@ internal static void ConfiguredEndPoints(ILogger logger, string serviceName, str endpointValues.Append(endpoints[i].ToString()); } - var configuredEndPoints = endpointValues.ToString(); - ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + var configuredEndpoints = endpointValues.ToString(); + ConfiguredEndpoints(logger, serviceName, path, configuredEndpoints); } [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs similarity index 76% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 9604ec0c201..37078e01969 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -10,36 +10,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// A service endpoint resolver that uses configuration to resolve resolved. +/// A service endpoint provider that uses configuration to resolve resolved. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature +internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEndpointProvider, IHostNameFeature { - private const string DefaultEndPointName = "default"; + private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; private readonly string[] _schemes; private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IOptions _options; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The query. /// The configuration. /// The logger. - /// Configuration resolver options. + /// Configuration provider options. /// Service discovery options. - public ConfigurationServiceEndPointResolver( - ServiceEndPointQuery query, + public ConfigurationServiceEndpointProvider( + ServiceEndpointQuery query, IConfiguration configuration, - ILogger logger, - IOptions options, + ILogger logger, + IOptions options, IOptions serviceDiscoveryOptions) { _serviceName = query.ServiceName; - _endpointName = query.EndPointName; - _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); + _endpointName = query.EndpointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -49,10 +49,10 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + 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) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return default; @@ -62,12 +62,12 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - endPoints.AddChangeToken(_configuration.GetReloadToken()); + endpoints.AddChangeToken(_configuration.GetReloadToken()); Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); return default; } - endPoints.AddChangeToken(section.GetReloadToken()); + endpoints.AddChangeToken(section.GetReloadToken()); // Find an appropriate configuration section based on the input. IConfigurationSection? namedSection = null; @@ -77,7 +77,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (_schemes.Length == 0) { // Use the section named "default". - endpointName = DefaultEndPointName; + endpointName = DefaultEndpointName; namedSection = section.GetSection(endpointName); } else @@ -112,14 +112,14 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo return default; } - List resolved = []; + List resolved = []; Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); // Account for both the single and multi-value cases. if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - AddEndPoint(resolved, namedSection, endpointName); + AddEndpoint(resolved, namedSection, endpointName); } else { @@ -131,7 +131,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - AddEndPoint(resolved, child, endpointName); + AddEndpoint(resolved, child, endpointName); } } @@ -158,13 +158,13 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (index >= 0 && index <= minIndex) { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } else { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } @@ -174,7 +174,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo } else { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); } return default; @@ -182,7 +182,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo string IHostNameFeature.HostName => _serviceName; - private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) + private void AddEndpoint(List endpoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) @@ -190,7 +190,7 @@ private void AddEndPoint(List endPoints, IConfigurationSection throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } - endPoints.Add(CreateEndPoint(endPoint)); + endpoints.Add(CreateEndpoint(endPoint)); } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -220,16 +220,16 @@ private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPo return true; } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + private ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (_options.Value.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } public override string ToString() => "Configuration"; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs similarity index 58% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs index 032c50b6f27..a966cd44794 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs @@ -9,18 +9,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// -internal sealed class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndpointProviderFactory( IConfiguration configuration, - IOptions options, + IOptions options, IOptions serviceDiscoveryOptions, - ILogger logger) : IServiceEndPointProviderFactory + ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); + provider = new ConfigurationServiceEndpointProvider(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs index 91e97b5d0bc..f8092c4dd51 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs @@ -1,22 +1,22 @@ -// 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.Configuration; -internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +internal sealed class ConfigurationServiceEndpointProviderOptionsValidator : IValidateOptions { - public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndpointProviderOptions options) { if (string.IsNullOrWhiteSpace(options.SectionName)) { return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); } - if (options.ApplyHostNameMetadata is null) + if (options.ShouldApplyHostNameMetadata is null) { - return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + return ValidateOptionsResult.Fail($"{nameof(options.ShouldApplyHostNameMetadata)} must not be null."); } return ValidateOptionsResult.Success; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs index d3b94f2f1e7..29f28e359f7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for . +/// Options for . /// -public sealed class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndpointProviderOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". @@ -18,5 +18,5 @@ public sealed class ConfigurationServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index 44e58b0dbbb..e11f593776f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -11,13 +11,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); + 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 ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; + private readonly ServiceEndpointWatcherFactory _watcherFactory = watcherFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -29,7 +29,7 @@ internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. - public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); if (request.RequestUri is null) @@ -47,15 +47,15 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ static (name, self) => self.CreateResolver(name), this); - var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + var (valid, endpoint) = await resolver.TryGetEndpointAsync(request, cancellationToken).ConfigureAwait(false); if (valid) { - if (endPoint is null) + if (endpoint is null) { throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); } - return endPoint; + return endpoint; } } } @@ -148,37 +148,37 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateWatcher(serviceName); - var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); - var result = new ResolverEntry(resolver, selector); - resolver.Start(); + var watcher = _watcherFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndpointSelector(); + var result = new ResolverEntry(watcher, selector); + watcher.Start(); return result; } private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver; - private readonly IServiceEndPointSelector _selector; + private readonly ServiceEndpointWatcher _watcher; + private readonly IServiceEndpointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndpointWatcher watcher, IServiceEndpointSelector selector) { - _resolver = resolver; + _watcher = watcher; _selector = selector; - _resolver.OnEndPointsUpdated += result => + _watcher.OnEndpointsUpdated += result => { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPointSource); + _selector.SetEndpoints(result.EndpointSource); } }; } - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -189,17 +189,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpoint? Endpoint)> TryGetEndpointAsync(object? context, CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - var result = _selector.GetEndPoint(context); + await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndpoint(context); return (true, result); } else @@ -246,7 +246,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs index 0febfa94815..0c5bd02d10d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.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. namespace Microsoft.Extensions.ServiceDiscovery.Http; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index bc06a031700..a0063ae476b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndpointResolver resolver, IOptions options) : HttpClientHandler { - private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly HttpServiceEndpointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; /// @@ -23,7 +23,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index daa7b8a17de..8f13bb60ab5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { - private readonly HttpServiceEndPointResolver _resolver; + private readonly HttpServiceEndpointResolver _resolver; private readonly ServiceDiscoveryOptions _options; /// @@ -19,7 +19,7 @@ internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler /// /// The endpoint resolver. /// The service discovery options. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options) { _resolver = resolver; _options = options.Value; @@ -31,7 +31,7 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt /// The endpoint resolver. /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; _options = options.Value; @@ -44,7 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } @@ -58,11 +58,11 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) + internal static Uri GetUriWithEndpoint(Uri uri, ServiceEndpoint serviceEndpoint, ServiceDiscoveryOptions options) { - var endpoint = serviceEndPoint.EndPoint; + var endPoint = serviceEndpoint.EndPoint; UriBuilder result; - if (endpoint is UriEndPoint { Uri: { } ep }) + if (endPoint is UriEndPoint { Uri: { } ep }) { result = new UriBuilder(uri) { @@ -84,7 +84,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, { string host; int port; - switch (endpoint) + switch (endPoint) { case IPEndPoint ip: host = ip.Address.ToString(); @@ -95,7 +95,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, port = dns.Port; break; default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + throw new InvalidOperationException($"Endpoints of type {endPoint.GetType()} are not supported"); } result = new UriBuilder(uri) @@ -112,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.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowAllSchemes || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs index 0d3ba00122f..e5e7f7587bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -8,12 +8,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( TimeProvider timeProvider, IServiceProvider serviceProvider, - ServiceEndPointWatcherFactory factory, + ServiceEndpointWatcherFactory factory, IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory { public HttpMessageHandler CreateHandler(HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + var registry = new HttpServiceEndpointResolver(factory, serviceProvider, timeProvider); return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs index 07bffa5654b..675941bb955 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; /// /// Represents the result of service endpoint resolution. /// -/// The endpoint collection. +/// The endpoint collection. /// The exception which occurred during resolution. -internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +internal sealed class ServiceEndpointResolverResult(ServiceEndpointSource? endpointSource, Exception? exception) { /// /// Gets the exception which occurred during resolution. @@ -20,11 +20,11 @@ internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPo /// /// Gets a value indicating whether resolution completed successfully. /// - [MemberNotNullWhen(true, nameof(EndPointSource))] + [MemberNotNullWhen(true, nameof(EndpointSource))] public bool ResolvedSuccessfully => Exception is null; /// /// Gets the endpoints. /// - public ServiceEndPointSource? EndPointSource { get; } = endPointSource; + public ServiceEndpointSource? EndpointSource { get; } = endpointSource; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs index bd0172c45cf..2d81ff38601 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs @@ -6,18 +6,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -internal 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(ServiceEndPointSource endPoints); + /// The collection of endpoints to select from. + 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. - ServiceEndPoint GetEndPoint(object? context); + ServiceEndpoint GetEndpoint(object? context); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs similarity index 63% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs index 92da7cf25bf..e7e51bc6021 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs @@ -6,21 +6,21 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndpointSelector : IServiceEndpointSelector { private uint _next; - private IReadOnlyList? _endPoints; + private IReadOnlyList? _endpoints; /// - public void SetEndPoints(ServiceEndPointSource endPoints) + public void SetEndpoints(ServiceEndpointSource endpoints) { - _endPoints = endPoints.EndPoints; + _endpoints = endpoints.Endpoints; } /// - public ServiceEndPoint GetEndPoint(object? context) + public ServiceEndpoint GetEndpoint(object? context) { - if (_endPoints is not { Count: > 0 } collection) + if (_endpoints is not { Count: > 0 } collection) { throw new InvalidOperationException("The endpoint collection contains no endpoints"); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index 570eb5e4e47..f9a984cfe4f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -5,11 +5,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; -internal sealed partial class PassThroughServiceEndPointResolver +internal sealed partial class PassThroughServiceEndpointProvider { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs index 483c08702df..478d81d42dc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs @@ -7,18 +7,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver which passes through the provided value. +/// Service endpoint provider which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider +internal sealed partial class PassThroughServiceEndpointProvider(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count == 0) + if (endpoints.Endpoints.Count == 0) { Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); + var ep = ServiceEndpoint.Create(endPoint); + ep.Features.Set(this); + endpoints.Endpoints.Add(ep); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs index 83455e0979c..2bf8c0cb481 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs @@ -8,29 +8,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver provider which passes through the provided value. +/// Service endpoint provider factory which creates pass-through providers. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory +internal sealed class PassThroughServiceEndpointProviderFactory(ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - var serviceName = query.OriginalString; + var serviceName = query.ToString()!; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); + provider = new PassThroughServiceEndpointProvider(logger, serviceName, endPoint); return true; } - private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? endPoint) { if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) { - serviceEndPoint = null; + endPoint = null; return false; } @@ -50,15 +50,15 @@ private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] ou var port = uri.Port > 0 ? uri.Port : 0; if (IPAddress.TryParse(host, out var ip)) { - serviceEndPoint = new IPEndPoint(ip, port); + endPoint = new IPEndPoint(ip, port); } else if (!string.IsNullOrEmpty(host)) { - serviceEndPoint = new DnsEndPoint(host, port); + endPoint = new DnsEndPoint(host, port); } else { - serviceEndPoint = null; + endPoint = null; return false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 8bece9644ff..04119540e10 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -6,29 +6,27 @@ In typical systems, service configuration changes over time. Service discovery a ## How it works -Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). +Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. -Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. +Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. ### Change notifications -Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndpointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). ### Extensibility using features -Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: +Service endpoints (`ServiceEndpoint` instances) and collections of service endpoints (`ServiceEndpointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by providers. Features which may be available on a `ServiceEndpoint` include: * `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). -* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. -* `IEndPointLoadFeature`: used to query estimated endpoint load. ### Resolution order -The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. +The providers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. ## Getting Started @@ -42,19 +40,19 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); ``` -Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: +Add service discovery to an individual `IHttpClientBuilder` by calling the `AddServiceDiscovery` extension method: ```csharp builder.Services.AddHttpClient(c => { c.BaseAddress = new("http://catalog")); -}).UseServiceDiscovery(); +}).AddServiceDiscovery(); ``` Alternatively, you can add service discovery to all `HttpClient` instances by default: @@ -63,14 +61,14 @@ Alternatively, you can add service discovery to all `HttpClient` instances by de builder.Services.ConfigureHttpClientDefaults(http => { // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); ``` ### Resolving service endpoints from configuration -The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. -This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The `AddServiceDiscovery` extension method adds a configuration-based endpoint provider by default. +This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: @@ -89,30 +87,30 @@ Here is an example demonstrating how to configure a endpoints for the service na The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. ### Configuration -The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: +The configuration provider is configured using the `ConfigurationServiceEndpointProviderOptions` class, which offers these configuration options: * **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. -* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. +* **`ShouldApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(options => +builder.Services.Configure(options => { options.SectionName = "MyServiceEndpoints"; // Configure the logic for applying host name metadata - options.ApplyHostNameMetadata = endpoint => + options.ShouldApplyHostNameMetadata = endpoint => { // Your custom logic here. For example: - return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + return endpoint.Endpoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); }; }); ``` @@ -121,38 +119,38 @@ This example demonstrates setting a custom section name for your service endpoin ## Resolving service endpoints using platform-provided service discovery -Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. -The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. +The pass-through provider performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndpointProvider` extension method on `IServiceCollection`. In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". ## Load-balancing with endpoint selectors -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: ```csharp builder.Services.AddHttpClient( static client => client.BaseAddress = new("http://catalog")); - .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); + .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); ``` The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` -Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. +Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. ## Service discovery in .NET Aspire -.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: @@ -184,7 +182,7 @@ In the above example, two `HttpClient`s are added: one for the core basket servi ### Named endpoints using configuration -With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": +With the configuration-based endpoint provider, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": ```json { @@ -199,7 +197,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified ### Named endpoints in .NET Aspire -.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: +.NET Aspire uses the configuration-based provider at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: ```csharp var builder = DistributedApplication.CreateBuilder(args); @@ -208,13 +206,13 @@ var basket = builder.AddProject("basket") .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") - .WithReference(basket.GetEndPoint("admin")); + .WithReference(basket.GetEndpoint("admin")); var frontend = builder.AddProject("frontend") .WithReference(basket); ``` -In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndpoint(string name)` method, as in the following example: ```csharp @@ -226,7 +224,7 @@ var frontend = builder.AddProject("frontend") ### Named endpoints in Kubernetes using DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -244,11 +242,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index b4c34ccb7c5..8a137aad4f8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -26,8 +26,8 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); + var watcherFactory = services.GetRequiredService(); + var registry = new HttpServiceEndpointResolver(watcherFactory, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 89c5a2d2eb0..02ce1af162b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -6,21 +6,19 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for service endpoint resolvers. +/// Options for service endpoint resolution. /// public sealed class ServiceDiscoveryOptions { /// - /// The value indicating that all endpoint schemes are allowed. + /// Gets or sets a value indicating whether all URI schemes for URIs resolved by the service discovery system are allowed. + /// If this value is , all URI schemes are allowed. + /// If this value is , only the schemes specified in are allowed. /// -#pragma warning disable IDE0300 // Simplify collection initialization -#pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllowAllSchemes = new string[0]; -#pragma warning restore CA1825 // Avoid zero-length array allocations -#pragma warning restore IDE0300 // Simplify collection initialization + public bool AllowAllSchemes { get; set; } = true; /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// Gets or sets the period between polling attempts for providers which do not support refresh notifications via . /// public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); @@ -28,14 +26,13 @@ public sealed class ServiceDiscoveryOptions /// 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. - /// Schemes are not case-sensitive. + /// When is , this property is ignored. /// - public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + public IList AllowedSchemes { get; set; } = new List(); - internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowedSchemes.Equals(AllowAllSchemes)) + if (allowAllSchemes) { if (schemes is string[] array) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index 6403b214631..a5d789b7e4e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -26,8 +26,8 @@ public static class ServiceDiscoveryServiceCollectionExtensions public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { return services.AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -36,11 +36,11 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action configureOptions) { return services.AddServiceDiscoveryCore(configureOptions: configureOptions) - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -56,16 +56,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(_ => TimeProvider.System); - services.TryAddTransient(); - services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(sp => new ServiceEndpointResolver(sp.GetRequiredService(), sp.GetRequiredService())); if (configureOptions is not null) { services.Configure(configureOptions); @@ -75,26 +75,26 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { - return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); + services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); @@ -104,14 +104,14 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS } /// - /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// Configures a service discovery endpoint provider which passes through the input without performing resolution. /// /// The service collection. /// The service collection. - public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs deleted file mode 100644 index 90f62ab0597..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs +++ /dev/null @@ -1,61 +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.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.PassThrough; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates service endpoint watchers. -/// -internal sealed partial class ServiceEndPointWatcherFactory( - IEnumerable resolvers, - ILogger resolverLogger, - IOptions options, - TimeProvider timeProvider) -{ - 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; - - /// - /// Creates a service endpoint resolver for the provided service name. - /// - 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.TryCreateProvider(query, out var resolver)) - { - resolvers ??= []; - resolvers.Add(resolver); - } - } - - if (resolvers is not { Count: > 0 }) - { - throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); - } - - Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointWatcher( - resolvers: [.. resolvers], - logger: _logger, - serviceName: serviceName, - timeProvider: _timeProvider, - options: _options); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs index 1a14cb961b7..947f24b2f81 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs @@ -9,9 +9,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// A mutable collection of service endpoints. /// -internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +internal sealed class ServiceEndpointBuilder : IServiceEndpointBuilder { - private readonly List _endPoints = new(); + private readonly List _endpoints = new(); private readonly List _changeTokens = new(); private readonly FeatureCollection _features = new FeatureCollection(); @@ -32,15 +32,15 @@ public void AddChangeToken(IChangeToken changeToken) /// /// Gets the endpoints. /// - public IList EndPoints => _endPoints; + public IList Endpoints => _endpoints; /// - /// Creates a from the provided instance. + /// Creates a from the provided instance. /// /// The service endpoint source. - public ServiceEndPointSource Build() + public ServiceEndpointSource Build() { - return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + return new ServiceEndpointSource(_endpoints, new CompositeChangeToken(_changeTokens), _features); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 029d2601243..92df120940d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -9,13 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves service names to collections of endpoints. /// -public sealed class ServiceEndPointResolver : IAsyncDisposable +public sealed class ServiceEndpointResolver : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverProvider; + private readonly ServiceEndpointWatcherFactory _watcherFactory; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -23,13 +23,13 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The resolver factory. + /// The watcher factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, TimeProvider timeProvider) { - _resolverProvider = resolverProvider; + _watcherFactory = watcherFactory; _timeProvider = timeProvider; } @@ -39,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointWatcherFactory 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); @@ -54,7 +54,7 @@ public async ValueTask GetEndPointsAsync(string serviceNa static (name, self) => self.CreateResolver(name), this); - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var (valid, result) = await resolver.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); if (valid) { if (result is null) @@ -156,21 +156,21 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateWatcher(serviceName); + var resolver = _watcherFactory.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } - private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable + private sealed class ResolverEntry(ServiceEndpointWatcher watcher) : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver = resolver; + private readonly ServiceEndpointWatcher _watcher = watcher; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -181,17 +181,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpointSource? Endpoints)> GetEndpointsAsync(CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); + var endpoints = await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endpoints); } else { @@ -237,7 +237,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index 78a8f84b556..fce9f667b40 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -5,35 +5,35 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcher +partial class ServiceEndpointWatcher { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] - public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] + public static partial void ResolvingEndpoints(ILogger logger, string serviceName); [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] public static partial void ResolutionPending(ILogger logger, string serviceName); - [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); + [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, ServiceEndPointSource endPointSource) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndpointSource endpointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endpointSource.Endpoints.Count, serviceName, string.Join(", ", endpointSource.Endpoints.Select(GetEndpointString))); } - static string GetEndPointString(ServiceEndPoint ep) + static string GetEndpointString(ServiceEndpoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } provider) { - return $"{ep.GetEndPointString()} ({resolver})"; + return $"{ep} ({provider})"; } - return ep.GetEndPointString(); - } + return ep.ToString()!; + } } [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index 9b1069d31e7..ba6df9b43c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -13,23 +13,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -internal sealed partial class ServiceEndPointWatcher( - IServiceEndPointProvider[] resolvers, +internal sealed partial class ServiceEndpointWatcher( + IServiceEndpointProvider[] providers, ILogger logger, string serviceName, TimeProvider timeProvider, IOptions options) : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); + 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 ServiceDiscoveryOptions _options = options.Value; - private readonly IServiceEndPointProvider[] _resolvers = resolvers; + private readonly IServiceEndpointProvider[] _providers = providers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointSource? _cachedEndPoints; + private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -41,14 +41,14 @@ internal sealed partial class ServiceEndPointWatcher( /// /// Gets or sets the action called when endpoints are updated. /// - public Action? OnEndPointsUpdated { get; set; } + public Action? OnEndpointsUpdated { get; set; } /// /// Starts the endpoint resolver. /// public void Start() { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); _ = RefreshAsync(force: false); } @@ -57,27 +57,27 @@ 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(); + ThrowIfNoProviders(); // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + 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); + return GetEndpointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { - ServiceEndPointSource? result; + ServiceEndpointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; + result = _cachedEndpoints; } while (result is null); return result; } @@ -89,7 +89,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -124,27 +124,27 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointSource? newEndPoints = null; + ServiceEndpointSource? newEndpoints = null; CacheStatus newCacheState; try { - Log.ResolvingEndPoints(_logger, ServiceName); - var builder = new ServiceEndPointBuilder(); - foreach (var resolver in _resolvers) + Log.ResolvingEndpoints(_logger, ServiceName); + var builder = new ServiceEndpointBuilder(); + foreach (var provider in _providers) { - await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } - var endPoints = builder.Build(); + 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 (endpoints.ChangeToken.ActiveChangeCallbacks) { // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); if (_pollingTimer is { } timer) { _pollingTimer = null; @@ -157,7 +157,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = endPoints; + newEndpoints = endpoints; newCacheState = CacheStatus.Valid; } } @@ -171,26 +171,26 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // To ensure coherence between the value returned by calls made to GetEndpointsAsync and value passed to the callback, // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // before receiving the updated value. An alternative approach is to lock access to _cachedEndpoints, but // that will have more overhead in the common case. if (newCacheState is CacheStatus.Valid) { - Interlocked.Exchange(ref _cachedEndPoints, null); + Interlocked.Exchange(ref _cachedEndpoints, null); } - if (OnEndPointsUpdated is { } callback) + if (OnEndpointsUpdated is { } callback) { - callback(new(newEndPoints, error)); + callback(new(newEndpoints, error)); } lock (_lock) { if (newCacheState is CacheStatus.Valid) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + Debug.Assert(newEndpoints is not null); + _cachedEndpoints = newEndpoints; } _cacheState = newCacheState; @@ -201,9 +201,9 @@ private async Task RefreshAsyncInternal() Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (newEndPoints is not null) + else if (newEndpoints is not null) { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndpoints); } } @@ -240,9 +240,9 @@ public async ValueTask DisposeAsync() await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } - foreach (var resolver in _resolvers) + foreach (var provider in _providers) { - await resolver.DisposeAsync().ConfigureAwait(false); + await provider.DisposeAsync().ConfigureAwait(false); } } @@ -253,14 +253,14 @@ private enum CacheStatus Valid } - private void ThrowIfNoResolvers() + private void ThrowIfNoProviders() { - if (_resolvers.Length == 0) + if (_providers.Length == 0) { - ThrowNoResolversConfigured(); + ThrowNoProvidersConfigured(); } } [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); + private static void ThrowNoProvidersConfigured() => throw new InvalidOperationException("No service endpoint providers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 69f565eb8e3..5f4acc89874 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -5,17 +5,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcherFactory +partial class ServiceEndpointWatcherFactory { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] - public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); + + public static void CreatingResolver(ILogger logger, string serviceName, List providers) { if (logger.IsEnabled(LogLevel.Debug)) { - ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); + ServiceEndpointProviderListCore(logger, serviceName, providers.Count, string.Join(", ", providers.Select(static r => r.ToString()))); } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs new file mode 100644 index 00000000000..6cc7cb2cbc5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs @@ -0,0 +1,61 @@ +// 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.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates service endpoint watchers. +/// +internal sealed partial class ServiceEndpointWatcherFactory( + IEnumerable providerFactories, + ILogger logger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndpointProviderFactory[] _providerFactories = providerFactories + .Where(r => r is not PassThroughServiceEndpointProviderFactory) + .Concat(providerFactories.Where(static r => r is PassThroughServiceEndpointProviderFactory)).ToArray(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a service endpoint watcher for the provided service name. + /// + 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? providers = null; + foreach (var factory in _providerFactories) + { + if (factory.TryCreateProvider(query, out var provider)) + { + providers ??= []; + providers.Add(provider); + } + } + + if (providers is not { Count: > 0 }) + { + throw new InvalidOperationException($"No provider which supports the provided service name, '{serviceName}', has been configured."); + } + + Log.CreatingResolver(_logger, serviceName, providers); + return new ServiceEndpointWatcher( + providers: [.. providers], + logger: _logger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs similarity index 72% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 25cd88a1436..7cadb4e3c7f 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for and . +/// These also cover and by extension. /// -public class DnsSrvServiceEndPointResolverTests +public class DnsSrvServiceEndpointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndPoint_Dns() + public async Task ResolveServiceEndpoint_Dns() { var dnsClientMock = new FakeDnsClient { @@ -101,26 +101,26 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -134,7 +134,7 @@ public async Task ResolveServiceEndPoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -175,28 +175,28 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => + .AddDnsSrvServiceEndpointProvider(options => { options.QuerySuffix = ".ns"; - options.ApplyHostNameMetadata = _ => true; + options.ShouldApplyHostNameMetadata = _ => true; }) - .AddConfigurationServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider(); } else { serviceCollection - .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + .AddConfigurationServiceEndpointProvider() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); @@ -205,13 +205,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -221,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.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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -248,59 +248,4 @@ public void SetValues(IEnumerable> values) OnReload(); } } - - /* - [Fact] - public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() - { - var oneEndPoint = new Dictionary - { - ["services:basket:http:0:host"] = "localhost", - ["services:basket:http:0:port"] = "8080", - }; - var bothEndPoints = new Dictionary(oneEndPoint) - { - ["services:basket:http:1:host"] = "remotehost", - ["services:basket:http:1:port"] = "9090", - }; - var configSource = new MyConfigurationProvider(); - var services = new ServiceCollection() - .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) - .AddServiceDiscovery() - .AddConfigurationServiceEndPointResolver() - .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) - { - Assert.NotNull(resolver); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); - resolver.Start(); - var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialResult); - Assert.False(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); - Assert.Null(initialResult.EndPoints); - - // Update the config and check that it flows through the system. - configSource.SetValues(oneEndPoint); - - // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to - // cause an indefinite test hang. We expect the result to be published practically immediately, though. - _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); - var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - var firstEp = Assert.Single(oneEpResult); - Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); - - // Do it again to check that an updated (not cached) version is published. - configSource.SetValues(bothEndPoints); - var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.True(twoEpResult.ResolvedSuccessfully); - Assert.Equal(2, twoEpResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); - } - } - */ } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6d8091f026c..db720782107 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -12,13 +12,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class ConfigurationServiceEndPointResolverTests +public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -27,23 +27,23 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -52,7 +52,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() { // Try to resolve an http endpoint when only https is allowed. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary @@ -63,59 +63,63 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .Configure(o => o.AllowedSchemes = ["https"]) + .AddConfigurationServiceEndpointProvider() + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Empty(initialResult.EndPointSource.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.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.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.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { var configSource = new MemoryConfigurationSource { @@ -129,24 +133,24 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) + .AddConfigurationServiceEndpointProvider(options => options.ShouldApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -155,20 +159,20 @@ 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.CreateWatcher("https+http://basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -178,7 +182,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() { var configSource = new MemoryConfigurationSource { @@ -196,25 +200,25 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -223,7 +227,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() { var configSource = new MemoryConfigurationSource { @@ -241,29 +245,29 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.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.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.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.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index 643bbfad441..e0af5c03ed4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -12,36 +12,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class PassThroughServiceEndPointResolverTests +public class PassThroughServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_PassThrough() + public async Task ResolveServiceEndpoint_PassThrough() { var services = new ServiceCollection() .AddServiceDiscoveryCore() - .AddPassThroughServiceEndPointResolver() + .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Superseded() + public async Task ResolveServiceEndpoint_Superseded() { var configSource = new MemoryConfigurationSource { @@ -55,26 +55,26 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Fallback() + public async Task ResolveServiceEndpoint_Fallback() { var configSource = new MemoryConfigurationSource { @@ -88,27 +88,27 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndpointSource.Endpoints[0].EndPoint); } } // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. [Fact] - public async Task ResolveServiceEndPoint_Fallback_NoScheme() + public async Task ResolveServiceEndpoint_Fallback_NoScheme() { var configSource = new MemoryConfigurationSource { @@ -123,8 +123,8 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); - var result = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); + var resolver = services.GetRequiredService(); + var result = await resolver.GetEndpointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.Endpoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs similarity index 62% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index f5e506a9b72..16950e67374 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -15,58 +15,58 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// -public class ServiceEndPointResolverTests +public class ServiceEndpointResolverTests { [Fact] - public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + public void ResolveServiceEndpoint_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + 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); + Assert.Equal("No provider which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] - public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + public async Task ServiceEndpointResolver_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - 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()); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + var watcher = new ServiceEndpointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); + var exception = Assert.Throws(watcher.Start); + Assert.Equal("No service endpoint providers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await watcher.GetEndpointsAsync()); + Assert.Equal("No service endpoint providers are configured.", exception.Message); } [Fact] - public void ResolveServiceEndPoint_NullServiceName_Throws() + public void ResolveServiceEndpoint_NullServiceName_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task AddServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoProviders_Throws() { var serviceCollection = new ServiceCollection(); 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); + Assert.Equal("No provider which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory + private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(query); @@ -74,58 +74,58 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou } } - private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndpointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) => resolveAsync(endpoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } [Fact] - public async Task ResolveServiceEndPoint() + public async Task ResolveServiceEndpoint() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + 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); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); - var endpoints = resolverResult.EndPointSource.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]); @@ -133,34 +133,34 @@ public async Task ResolveServiceEndPoint() } [Fact] - public async Task ResolveServiceEndPointOneShot() + public async Task ResolveServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + 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); @@ -169,36 +169,36 @@ public async Task ResolveServiceEndPointOneShot() } [Fact] - public async Task ResolveHttpServiceEndPointOneShot() + public async Task ResolveHttpServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var fakeResolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, 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"); - var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(endPoint); - var ip = Assert.IsType(endPoint.EndPoint); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endpoint); + var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -206,12 +206,12 @@ public async Task ResolveHttpServiceEndPointOneShot() } [Fact] - public async Task ResolveServiceEndPoint_ThrowOnReload() + public async Task ResolveServiceEndpoint_ThrowOnReload() { var sem = new SemaphoreSlim(0); var cts = new[] { new CancellationTokenSource() }; var throwOnNextResolve = new[] { false }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: async (collection, ct) => { await sem.WaitAsync(ct).ConfigureAwait(false); @@ -228,25 +228,25 @@ 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))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); - var initialEndPoints = await initialEndPointsTask; - Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints.EndPoints); + var initialEndpoints = await initialEndpointsTask; + Assert.NotNull(initialEndpoints); + Assert.Single(initialEndpoints.Endpoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -254,21 +254,21 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var exception = await Assert.ThrowsAsync(async () => { - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); }).ConfigureAwait(false); Assert.Equal("throwing", exception.Message); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + var channel = Channel.CreateUnbounded(); + watcher.OnEndpointsUpdated = result => channel.Writer.TryWrite(result); do { cts[0].Cancel(); sem.Release(1); - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); await resolveTask.ConfigureAwait(false); var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); if (next.ResolvedSuccessfully) @@ -277,11 +277,11 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } } while (true); - var task = resolver.GetEndPointsAsync(CancellationToken.None); + var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var result = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, result); - var sep = Assert.Single(result.EndPoints); + 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); From 95ea2e8b4c758aaa1887858b9050f7257ab41bb2 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Mon, 8 Apr 2024 15:06:14 -0700 Subject: [PATCH 39/77] Update README.md to reflect Service Discovery API changes (#3228) --- .../README.md | 76 ++++++++++--------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 04119540e10..6c47ee67507 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -51,7 +51,7 @@ Add service discovery to an individual `IHttpClientBuilder` by calling the `AddS ```csharp builder.Services.AddHttpClient(c => { - c.BaseAddress = new("http://catalog")); + c.BaseAddress = new("https://catalog")); }).AddServiceDiscovery(); ``` @@ -71,20 +71,22 @@ The `AddServiceDiscovery` extension method adds a configuration-based endpoint p This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. -Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: +Here is an example demonstrating how to configure endpoints for the service named _catalog_ via `appsettings.json`: ```json { "Services": { - "catalog": [ - "localhost:8080", - "10.46.24.90:80", + "catalog": { + "https": [ + "https://localhost:8443", + "https://10.46.24.90:443" ] } + } } ``` -The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. +The above example adds two endpoints for the service named _catalog_: `https://localhost:8443`, and `"https://10.46.24.90:443"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. @@ -117,6 +119,25 @@ builder.Services.Configure(options This example demonstrates setting a custom section name for your service endpoints and providing a custom logic for applying host name metadata based on a condition. +## Scheme selection when resolving HTTP(S) endpoints + +It is common to use HTTP while developing and testing a service locally and HTTPS when the service is deployed. Service Discovery supports this by allowing for a priority list of URI schemes to be specified in the input string given to Service Discovery. Service Discovery will attempt to resolve the services for the schemes in order and will stop after an endpoint is found. URI schemes are separated by a `+` character, for example: `"https+http://basket"`. Service Discovery will first try to find HTTPS endpoints for the `"basket"` service and will then fall back to HTTP endpoints. If any HTTPS endpoint is found, Service Discovery will not include HTTP endpoints. +Schemes can be filtered by configuring the `AllowedSchemes` and `AllowAllSchemes` properties on `ServiceDiscoveryOptions`. The `AllowAllSchemes` property is used to indicate that all schemes are allowed. By default, `AllowAllSchemes` is `true` and all schemes are allowed. Schemes can be restricted by setting `AllowAllSchemes` to `false` and adding allowed schemes to the `AllowedSchemes` property. For example, to allow only HTTPS: + +```csharp +services.Configure(options => +{ + options.AllowAllSchemes = false; + options.AllowedSchemes = ["https"]; +}); +``` + +To explicitly allow all schemes, set the `ServiceDiscoveryOptions.AllowAllSchemes` property to `true`: + +```csharp +services.Configure(options => options.AllowAllSchemes = true); +``` + ## Resolving service endpoints using platform-provided service discovery Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. @@ -129,25 +150,6 @@ If service discovery was added to the host using the `AddServiceDiscoveryCore` e In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". -## Load-balancing with endpoint selectors - -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: - -```csharp -builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://catalog")); - .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); -``` - -The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: - -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` - -Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. - ## Service discovery in .NET Aspire .NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. @@ -169,13 +171,13 @@ In the above example, the _frontend_ project references the _catalog_ project an ## Named endpoints -Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `http://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `http://_dashboard.basket` can be used to specify this endpoint, for example: +Some services expose multiple, named endpoints. Named endpoints can be resolved by specifying the endpoint name in the host portion of the HTTP request URI, following the format `scheme://_endpointName.serviceName`. For example, if a service named "basket" exposes an endpoint named "dashboard", then the URI `https+http://_dashboard.basket` can be used to specify this endpoint, for example: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://basket")); + static client => client.BaseAddress = new("https+http://basket")); builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://_dashboard.basket")); + static client => client.BaseAddress = new("https+http://_dashboard.basket")); ``` In the above example, two `HttpClient`s are added: one for the core basket service and one for the basket service's dashboard. @@ -187,10 +189,10 @@ With the configuration-based endpoint provider, named endpoints can be specified ```json { "Services": { - "basket": [ - "10.2.3.4:8080", /* the default endpoint, when resolving http://basket */ - "_dashboard.10.2.3.4:9999" /* the "dashboard" endpoint, resolved via http://_dashboard.basket */ - ] + "basket": + "https": "https://10.2.3.4:8080", /* the https endpoint, requested via https://basket */ + "dashboard": "https://10.2.3.4:9999" /* the "dashboard" endpoint, requested via https://_dashboard.basket */ + } } } ``` @@ -203,7 +205,7 @@ With the configuration-based endpoint provider, named endpoints can be specified var builder = DistributedApplication.CreateBuilder(args); var basket = builder.AddProject("basket") - .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); + .WithEndpoint(hostPort: 9999, scheme: "https", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") .WithReference(basket.GetEndpoint("admin")); @@ -219,7 +221,7 @@ In the above example, the "basket" service exposes an "admin" endpoint in additi // The preceding code is the same as in the above sample var frontend = builder.AddProject("frontend") - .WithReference(basket.GetEndpoint("http")); + .WithReference(basket.GetEndpoint("https")); ``` ### Named endpoints in Kubernetes using DNS SRV @@ -249,20 +251,20 @@ builder.Services.AddServiceDiscoveryCore(); builder.Services.AddDnsSrvServiceEndpointProvider(); ``` -The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. +The special port name "default" is used to specify the default endpoint, resolved using the URI `https://basket`. As in the previous example, add service discovery to an `HttpClient` for the basket service: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://basket")); + static client => client.BaseAddress = new("https://basket")); ``` Similarly, the "dashboard" endpoint can be targeted as follows: ```csharp builder.Services.AddHttpClient( - static client => client.BaseAddress = new("http://_dashboard.basket")); + static client => client.BaseAddress = new("https://_dashboard.basket")); ``` ### Named endpoints in Azure Container Apps From 616f098cf88b26920710a6a596042ba454711ed7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 08:37:57 +1000 Subject: [PATCH 40/77] [release/8.0] Service Discovery: Implement approved API (#3460) * Rename EndPoint to Endpoint, resolver to provider * Apply changes decided during API review * Find & fix straggler file names * Variables and members assignable to System.Net.EndPoint use upper-case P * Update src/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs Co-authored-by: Stephen Halter * Delete GetEndpointString() --------- Co-authored-by: Reuben Bond Co-authored-by: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Co-authored-by: Stephen Halter --- .../IServiceEndPointProviderFactory.cs | 20 -- ...tBuilder.cs => IServiceEndpointBuilder.cs} | 8 +- ...rovider.cs => IServiceEndpointProvider.cs} | 6 +- .../IServiceEndpointProviderFactory.cs | 20 ++ ...EndPointImpl.cs => ServiceEndpointImpl.cs} | 8 +- .../README.md | 2 +- ...{ServiceEndPoint.cs => ServiceEndpoint.cs} | 21 +-- ...dPointQuery.cs => ServiceEndpointQuery.cs} | 35 ++-- ...ointSource.cs => ServiceEndpointSource.cs} | 16 +- .../DnsServiceEndPointResolverProvider.cs | 21 --- ...olver.cs => DnsServiceEndpointProvider.cs} | 28 +-- ... => DnsServiceEndpointProviderBase.Log.cs} | 2 +- ...e.cs => DnsServiceEndpointProviderBase.cs} | 36 ++-- .../DnsServiceEndpointProviderFactory.cs | 21 +++ ...s => DnsServiceEndpointProviderOptions.cs} | 6 +- ...er.cs => DnsSrvServiceEndpointProvider.cs} | 34 ++-- ...> DnsSrvServiceEndpointProviderFactory.cs} | 19 +- ...> DnsSrvServiceEndpointProviderOptions.cs} | 6 +- .../README.md | 16 +- ...DiscoveryDnsServiceCollectionExtensions.cs | 33 +++- .../ServiceDiscoveryDestinationResolver.cs | 10 +- ...nfigurationServiceEndpointProvider.Log.cs} | 12 +- ...> ConfigurationServiceEndpointProvider.cs} | 64 +++---- ...gurationServiceEndpointProviderFactory.cs} | 12 +- ...erviceEndpointProviderOptionsValidator.cs} | 10 +- ...gurationServiceEndpointProviderOptions.cs} | 6 +- ...lver.cs => HttpServiceEndpointResolver.cs} | 46 ++--- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 2 +- .../Http/ResolvingHttpClientHandler.cs | 6 +- .../Http/ResolvingHttpDelegatingHandler.cs | 20 +- ...rviceDiscoveryHttpMessageHandlerFactory.cs | 4 +- ...lt.cs => ServiceEndpointResolverResult.cs} | 8 +- ...elector.cs => IServiceEndpointSelector.cs} | 10 +- ...s => RoundRobinServiceEndpointSelector.cs} | 12 +- ...PassThroughServiceEndpointProvider.Log.cs} | 4 +- ... => PassThroughServiceEndpointProvider.cs} | 14 +- ...sThroughServiceEndpointProviderFactory.cs} | 20 +- .../README.md | 76 ++++---- ...iceDiscoveryHttpClientBuilderExtensions.cs | 4 +- .../ServiceDiscoveryOptions.cs | 23 +-- ...iceDiscoveryServiceCollectionExtensions.cs | 40 ++-- .../ServiceEndPointWatcherFactory.cs | 61 ------ ...ntBuilder.cs => ServiceEndpointBuilder.cs} | 12 +- ...Resolver.cs => ServiceEndpointResolver.cs} | 36 ++-- ...r.Log.cs => ServiceEndpointWatcher.Log.cs} | 24 +-- ...ntWatcher.cs => ServiceEndpointWatcher.cs} | 80 ++++---- ...s => ServiceEndpointWatcherFactory.Log.cs} | 11 +- .../ServiceEndpointWatcherFactory.cs | 61 ++++++ ... => DnsSrvServiceEndpointResolverTests.cs} | 125 ++++-------- ...figurationServiceEndpointResolverTests.cs} | 178 +++++++++--------- ...assThroughServiceEndpointResolverTests.cs} | 74 ++++---- ...sts.cs => ServiceEndpointResolverTests.cs} | 150 +++++++-------- 52 files changed, 763 insertions(+), 810 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointBuilder.cs => IServiceEndpointBuilder.cs} (80%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{IServiceEndPointProvider.cs => IServiceEndpointProvider.cs} (75%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/{ServiceEndPointImpl.cs => ServiceEndpointImpl.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPoint.cs => ServiceEndpoint.cs} (52%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointQuery.cs => ServiceEndpointQuery.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/{ServiceEndPointSource.cs => ServiceEndpointSource.cs} (74%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolver.cs => DnsServiceEndpointProvider.cs} (61%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.Log.cs => DnsServiceEndpointProviderBase.Log.cs} (97%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverBase.cs => DnsServiceEndpointProviderBase.cs} (81%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsServiceEndPointResolverOptions.cs => DnsServiceEndpointProviderOptions.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolver.cs => DnsSrvServiceEndpointProvider.cs} (71%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverProvider.cs => DnsSrvServiceEndpointProviderFactory.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/{DnsSrvServiceEndPointResolverOptions.cs => DnsSrvServiceEndpointProviderOptions.cs} (86%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.Log.cs => ConfigurationServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolver.cs => ConfigurationServiceEndpointProvider.cs} (76%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverProvider.cs => ConfigurationServiceEndpointProviderFactory.cs} (58%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/{ConfigurationServiceEndPointResolverOptionsValidator.cs => ConfigurationServiceEndpointProviderOptionsValidator.cs} (62%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ConfigurationServiceEndPointResolverOptions.cs => ConfigurationServiceEndpointProviderOptions.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/{HttpServiceEndPointResolver.cs => HttpServiceEndpointResolver.cs} (82%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/{ServiceEndPointResolverResult.cs => ServiceEndpointResolverResult.cs} (73%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{IServiceEndPointSelector.cs => IServiceEndpointSelector.cs} (69%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/{RoundRobinServiceEndPointSelector.cs => RoundRobinServiceEndpointSelector.cs} (63%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.Log.cs => PassThroughServiceEndpointProvider.Log.cs} (78%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolver.cs => PassThroughServiceEndpointProvider.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/{PassThroughServiceEndPointResolverProvider.cs => PassThroughServiceEndpointProviderFactory.cs} (70%) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointBuilder.cs => ServiceEndpointBuilder.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointResolver.cs => ServiceEndpointResolver.cs} (84%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.Log.cs => ServiceEndpointWatcher.Log.cs} (60%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcher.cs => ServiceEndpointWatcher.cs} (75%) rename src/Libraries/Microsoft.Extensions.ServiceDiscovery/{ServiceEndPointWatcherFactory.Log.cs => ServiceEndpointWatcherFactory.Log.cs} (53%) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/{DnsSrvServiceEndPointResolverTests.cs => DnsSrvServiceEndpointResolverTests.cs} (72%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ConfigurationServiceEndPointResolverTests.cs => ConfigurationServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{PassThroughServiceEndPointResolverTests.cs => PassThroughServiceEndpointResolverTests.cs} (60%) rename test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/{ServiceEndPointResolverTests.cs => ServiceEndpointResolverTests.cs} (62%) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs deleted file mode 100644 index 4b1876f808e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProviderFactory.cs +++ /dev/null @@ -1,20 +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; - -/// -/// Creates instances. -/// -public interface IServiceEndPointProviderFactory -{ - /// - /// Tries to create an instance for the specified . - /// - /// The service to create the resolver for. - /// The resolver. - /// if the resolver was created, otherwise. - bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver); -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs similarity index 80% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs index 468adea1c09..e051b2bf746 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointBuilder.cs @@ -7,14 +7,14 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Builder to create a instances. +/// Builder to create a instances. /// -public interface IServiceEndPointBuilder +public interface IServiceEndpointBuilder { /// /// Gets the endpoints. /// - IList EndPoints { get; } + IList Endpoints { get; } /// /// Gets the feature collection. @@ -22,7 +22,7 @@ public interface IServiceEndPointBuilder IFeatureCollection Features { get; } /// - /// Adds a change token to the resulting . + /// Adds a change token to the resulting . /// /// The change token. void AddChangeToken(IChangeToken changeToken); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs index 950823257af..4a192180b66 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndPointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProvider.cs @@ -6,13 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Provides details about a service's endpoints. /// -public interface IServiceEndPointProvider : IAsyncDisposable +public interface IServiceEndpointProvider : IAsyncDisposable { /// /// Resolves the endpoints for the service. /// - /// The endpoint collection, which resolved endpoints will be added to. + /// The endpoint collection, which resolved endpoints will be added to. /// The token to monitor for cancellation requests. /// The resolution status. - ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken); + ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..009cbf05d76 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/IServiceEndpointProviderFactory.cs @@ -0,0 +1,20 @@ +// 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; + +/// +/// Creates instances. +/// +public interface IServiceEndpointProviderFactory +{ + /// + /// Tries to create an instance for the specified . + /// + /// The service to create the provider for. + /// The provider. + /// if the provider was created, otherwise. + bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 7d135dfe97d..8bfb50fe930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndPointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -6,9 +6,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; -internal sealed class ServiceEndPointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndPoint +internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); - public override string? ToString() => GetEndPointString(); + public override string? ToString() => EndPoint switch + { + DnsEndPoint dns => $"{dns.Host}:{dns.Port}", + _ => EndPoint.ToString()! + }; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md index c5cf6b9bc78..0d97211313e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/README.md @@ -1,6 +1,6 @@ # Microsoft.Extensions.ServiceDiscovery.Abstractions -The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint resolvers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). +The `Microsoft.Extensions.ServiceDiscovery.Abstractions` library provides abstractions used by the `Microsoft.Extensions.ServiceDiscovery` library and other libraries which implement service discovery extensions, such as service endpoint providers. For more information, see [Service discovery in .NET](https://learn.microsoft.com/dotnet/core/extensions/service-discovery). ## Feedback & contributing diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs similarity index 52% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index a3cde62ce0d..238e383a957 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.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 System.Diagnostics; using System.Net; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.ServiceDiscovery.Internal; @@ -11,8 +10,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Represents an endpoint for a service. /// -[DebuggerDisplay("{GetEndPointString(),nq}")] -public abstract class ServiceEndPoint +public abstract class ServiceEndpoint { /// /// Gets the endpoint. @@ -25,21 +23,10 @@ public abstract class ServiceEndPoint public abstract IFeatureCollection Features { get; } /// - /// Creates a new . + /// Creates a new . /// /// The endpoint being represented. /// Features of the endpoint. - /// A newly initialized . - public static ServiceEndPoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndPointImpl(endPoint, features); - - /// - /// Gets a string representation of the . - /// - /// A string representation of the . - public virtual string GetEndPointString() => EndPoint switch - { - DnsEndPoint dns => $"{dns.Host}:{dns.Port}", - IPEndPoint ip => ip.ToString(), - _ => EndPoint.ToString()! - }; + /// A newly initialized . + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 99c92cce27c..600dc5cc28c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -8,21 +8,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Describes a query for endpoints of a service. /// -public sealed class ServiceEndPointQuery +public sealed class ServiceEndpointQuery { + private readonly string _originalString; + /// - /// Initializes a new instance. + /// 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) + /// The optional endpoint name. + private ServiceEndpointQuery(string originalString, string[] includedSchemes, string serviceName, string? endpointName) { - OriginalString = originalString; - IncludeSchemes = includedSchemes; + _originalString = originalString; + IncludedSchemes = includedSchemes; ServiceName = serviceName; - EndPointName = endPointName; + EndpointName = endpointName; } /// @@ -31,7 +33,7 @@ private ServiceEndPointQuery(string originalString, string[] includedSchemes, st /// 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) + public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) @@ -52,10 +54,10 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin var uriHost = uri.Host; var segmentSeparatorIndex = uriHost.IndexOf('.'); string host; - string? endPointName = null; + string? endpointName = null; if (uriHost.StartsWith('_') && segmentSeparatorIndex > 1 && uriHost[^1] != '.') { - endPointName = uriHost[1..segmentSeparatorIndex]; + endpointName = uriHost[1..segmentSeparatorIndex]; // Skip the endpoint name, including its prefix ('_') and suffix ('.'). host = uriHost[(segmentSeparatorIndex + 1)..]; @@ -67,24 +69,19 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin // 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); + 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; } + public IReadOnlyList IncludedSchemes { get; } /// /// Gets the endpoint name, or if no endpoint name is specified. /// - public string? EndPointName { get; } + public string? EndpointName { get; } /// /// Gets the service name. @@ -92,6 +89,6 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndPoin 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)}"; + public override string? ToString() => _originalString; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs similarity index 74% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index 807981226e3..fb5bff1b288 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndPointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -11,18 +11,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// Represents a collection of service endpoints. /// [DebuggerDisplay("{ToString(),nq}")] -[DebuggerTypeProxy(typeof(ServiceEndPointCollectionDebuggerView))] -public sealed class ServiceEndPointSource +[DebuggerTypeProxy(typeof(ServiceEndpointCollectionDebuggerView))] +public sealed class ServiceEndpointSource { - private readonly List? _endpoints; + private readonly List? _endpoints; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The endpoints. /// The change token. /// The feature collection. - public ServiceEndPointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) + public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); @@ -34,7 +34,7 @@ public ServiceEndPointSource(List? endpoints, IChangeToken chan /// /// Gets the endpoints. /// - public IReadOnlyList EndPoints => _endpoints ?? (IReadOnlyList)[]; + public IReadOnlyList Endpoints => _endpoints ?? (IReadOnlyList)[]; /// /// Gets the change token which indicates when this collection should be refreshed. @@ -57,13 +57,13 @@ public override string ToString() return $"[{string.Join(", ", eps)}]"; } - private sealed class ServiceEndPointCollectionDebuggerView(ServiceEndPointSource value) + private sealed class ServiceEndpointCollectionDebuggerView(ServiceEndpointSource value) { public IChangeToken ChangeToken => value.ChangeToken; public IFeatureCollection Features => value.Features; [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public ServiceEndPoint[] EndPoints => value.EndPoints.ToArray(); + public ServiceEndpoint[] Endpoints => value.Endpoints.ToArray(); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs deleted file mode 100644 index 51525663a03..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverProvider.cs +++ /dev/null @@ -1,21 +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.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.ServiceDiscovery.Dns; - -internal sealed partial class DnsServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : IServiceEndPointProviderFactory -{ - /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) - { - resolver = new DnsServiceEndPointResolver(query.OriginalString, hostName: query.ServiceName, options, logger, timeProvider); - return true; - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs similarity index 61% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index a2601c84b45..6cc9f92bc46 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -7,12 +7,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsServiceEndpointProvider( + ServiceEndpointQuery query, string hostName, - IOptionsMonitor options, - ILogger logger, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; protected override TimeSpan MinRetryPeriod => options.CurrentValue.MinRetryPeriod; @@ -26,27 +26,27 @@ internal sealed partial class DnsServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); foreach (var address in addresses) { - var serviceEndPoint = ServiceEndPoint.Create(new IPEndPoint(address, 0)); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - endPoints.Add(serviceEndPoint); + endpoints.Add(serviceEndpoint); } - if (endPoints.Count == 0) + if (endpoints.Count == 0) { - throw new InvalidOperationException($"No DNS records were found for service {ServiceName} (DNS name: {hostName})."); + throw new InvalidOperationException($"No DNS records were found for service '{ServiceName}' (DNS name: '{hostName}')."); } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs similarity index 97% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs index cd664215aa9..29aaaf8e930 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.Log.cs @@ -5,7 +5,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -partial class DnsServiceEndPointResolverBase +partial class DnsServiceEndpointProviderBase { internal static partial class Log { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs similarity index 81% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 9d6c54e4755..6c69cc7a760 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -7,9 +7,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// A service end point resolver that uses DNS to resolve the service end points. +/// A service end point provider that uses DNS to resolve the service end points. /// -internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPointProvider +internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpointProvider { private readonly object _lock = new(); private readonly ILogger _logger; @@ -20,23 +20,23 @@ internal abstract partial class DnsServiceEndPointResolverBase : IServiceEndPoin private bool _hasEndpoints; private CancellationChangeToken _lastChangeToken; private CancellationTokenSource _lastCollectionCancellation; - private List? _lastEndPointCollection; + private List? _lastEndpointCollection; private TimeSpan _nextRefreshPeriod; /// - /// Initializes a new instance. + /// Initializes a new instance. /// - /// The service name. + /// The service name. /// The logger. /// The time provider. - protected DnsServiceEndPointResolverBase( - string serviceName, + protected DnsServiceEndpointProviderBase( + ServiceEndpointQuery query, ILogger logger, TimeProvider timeProvider) { - ServiceName = serviceName; + ServiceName = query.ToString()!; _logger = logger; - _lastEndPointCollection = null; + _lastEndpointCollection = null; _timeProvider = timeProvider; _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(); @@ -58,10 +58,10 @@ protected DnsServiceEndPointResolverBase( protected CancellationToken ShutdownToken => _disposeCancellation.Token; /// - public async ValueTask PopulateAsync(IServiceEndPointBuilder 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) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, ServiceName, "Collection has existing endpoints"); return; @@ -85,28 +85,28 @@ public async ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, Cancella lock (_lock) { - if (_lastEndPointCollection is { Count: > 0 } eps) + if (_lastEndpointCollection is { Count: > 0 } eps) { foreach (var ep in eps) { - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } - endPoints.AddChangeToken(_lastChangeToken); + endpoints.AddChangeToken(_lastChangeToken); return; } } - private bool ShouldRefresh() => _lastEndPointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; + private bool ShouldRefresh() => _lastEndpointCollection is null || _lastChangeToken is { HasChanged: true } || ElapsedSinceRefresh >= _nextRefreshPeriod; protected abstract Task ResolveAsyncCore(); - protected void SetResult(List endPoints, TimeSpan validityPeriod) + protected void SetResult(List endpoints, TimeSpan validityPeriod) { lock (_lock) { - if (endPoints is { Count: > 0 }) + if (endpoints is { Count: > 0 }) { _lastRefreshTimeStamp = _timeProvider.GetTimestamp(); _nextRefreshPeriod = DefaultRefreshPeriod; @@ -131,7 +131,7 @@ protected void SetResult(List endPoints, TimeSpan validityPerio _lastCollectionCancellation.Cancel(); var cancellation = _lastCollectionCancellation = new CancellationTokenSource(validityPeriod, _timeProvider); _lastChangeToken = new CancellationChangeToken(cancellation.Token); - _lastEndPointCollection = endPoints; + _lastEndpointCollection = endpoints; } TimeSpan GetRefreshPeriod() diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs new file mode 100644 index 00000000000..c241ad89dd3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -0,0 +1,21 @@ +// 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.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed partial class DnsServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, + TimeProvider timeProvider) : IServiceEndpointProviderFactory +{ + /// + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) + { + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs index 665c98bbc09..b163afc76ff 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsServiceEndPointResolverOptions +public class DnsServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -31,5 +31,5 @@ public class DnsServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs similarity index 71% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index d59dbfbb69c..dd17a7e2732 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -9,14 +9,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolver( - string serviceName, +internal sealed partial class DnsSrvServiceEndpointProvider( + ServiceEndpointQuery query, string srvQuery, string hostName, - IOptionsMonitor options, - ILogger logger, + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : DnsServiceEndPointResolverBase(serviceName, logger, timeProvider), IHostNameFeature + TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -32,7 +32,7 @@ internal sealed partial class DnsSrvServiceEndPointResolver( protected override async Task ResolveAsyncCore() { - var endPoints = new List(); + var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); @@ -59,15 +59,15 @@ protected override async Task ResolveAsyncCore() ttl = MinTtl(record, ttl); if (targetRecord is AddressRecord addressRecord) { - endPoints.Add(CreateEndPoint(new IPEndPoint(addressRecord.Address, record.Port))); + endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); } else if (targetRecord is CNameRecord canonicalNameRecord) { - endPoints.Add(CreateEndPoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); } } - SetResult(endPoints, ttl); + SetResult(endpoints, ttl); static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) { @@ -79,22 +79,22 @@ InvalidOperationException CreateException(string dnsName, string errorMessage) { var msg = errorMessage switch { - { Length: > 0 } => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName}): {errorMessage}.", - _ => $"No DNS records were found for service {ServiceName} (DNS name: {dnsName})." + { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", + _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." }; return new InvalidOperationException(msg); } - ServiceEndPoint CreateEndPoint(EndPoint endPoint) + ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (options.CurrentValue.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 8a75c1d1bbf..fd0cb28353d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -8,11 +8,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; -internal sealed partial class DnsSrvServiceEndPointResolverProvider( - IOptionsMonitor options, - ILogger logger, +internal sealed partial class DnsSrvServiceEndpointProviderFactory( + IOptionsMonitor options, + ILogger logger, IDnsQuery dnsClient, - TimeProvider timeProvider) : IServiceEndPointProviderFactory + 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"); @@ -20,7 +20,7 @@ internal sealed partial class DnsSrvServiceEndPointResolverProvider( private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // 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 @@ -30,17 +30,16 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou // 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); - resolver = default; + DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); + provider = default; return false; } - var portName = query.EndPointName ?? "default"; + var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - resolver = new DnsSrvServiceEndPointResolver(serviceName, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs similarity index 86% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index 704e03cd9ca..c908c56d770 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns; /// -/// Options for configuring . +/// Options for configuring . /// -public class DnsSrvServiceEndPointResolverOptions +public class DnsSrvServiceEndpointProviderOptions { /// /// Gets or sets the default refresh period for endpoints resolved from DNS. @@ -39,5 +39,5 @@ public class DnsSrvServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md index d3fbe2a75e5..8be4560870b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/README.md @@ -1,25 +1,25 @@ # Microsoft.Extensions.ServiceDiscovery.Dns -This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint resolvers: +This library provides support for resolving service endpoints using DNS (Domain Name System). It provides two service endpoint providers: -- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +- _DNS_, which resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. - _DNS SRV_, which resolves service names using DNS SRV record queries. This allows it to resolve both IP addresses and port numbers. This is useful for environments which support DNS SRV queries, such as Kubernetes (when configured accordingly). ## Resolving service endpoints with DNS -The _DNS_ resolver resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS resolver is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. +The _DNS_ service endpoint provider resolves endpoints using DNS `A/AAAA` record queries. This means that it can resolve names to IP addresses, but cannot resolve port numbers endpoints. As such, port numbers are assumed to be the default for the protocol (for example, 80 for HTTP and 433 for HTTPS). The benefit of using the DNS service endpoint provider is that for cases where these default ports are appropriate, clients can spread their requests across hosts. For more information, see _Load-balancing with endpoint selectors_. -To configure the DNS resolver in your application, add the DNS resolver to your host builder's service collection using the `AddDnsServiceEndPointResolver` method. service discovery as follows: +To configure the DNS service endpoint provider in your application, add the DNS service endpoint provider to your host builder's service collection using the `AddDnsServiceEndpointProvider` method. service discovery as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsServiceEndPointResolver(); +builder.Services.AddDnsServiceEndpointProvider(); ``` ## Resolving service endpoints in Kubernetes with DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -37,11 +37,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 0d795660fd2..17544d09486 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -14,6 +14,17 @@ namespace Microsoft.Extensions.Hosting; /// public static class ServiceDiscoveryDnsServiceCollectionExtensions { + /// + /// Adds DNS SRV service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. + /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. + /// + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + /// /// Adds DNS SRV service discovery to the . /// @@ -24,16 +35,26 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); services.TryAddSingleton(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } + /// + /// Adds DNS service discovery to the . + /// + /// The service collection. + /// The provided . + /// + /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. + /// + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + /// /// Adds DNS service discovery to the . /// @@ -43,11 +64,11 @@ public static IServiceCollection AddDnsSrvServiceEndPointResolver(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndPointResolver(this IServiceCollection services, Action? configureOptions = null) + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - var options = services.AddOptions(); + services.AddSingleton(); + var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 22d7e6d8327..8ec810f2c05 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndPointResolver resolver) : IDestinationResolver +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver { /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) @@ -54,14 +54,14 @@ public async ValueTask ResolveDestinationsAsync(I var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); - var result = await resolver.GetEndPointsAsync(serviceName, cancellationToken).ConfigureAwait(false); - var results = new List<(string Name, DestinationConfig Config)>(result.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 result.EndPoints) + foreach (var endpoint in result.Endpoints) { - var addressString = endPoint.GetEndPointString(); + var addressString = endpoint.ToString()!; Uri uri; if (!addressString.Contains("://")) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index fdb61ef59f4..48c922a85b3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -6,7 +6,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; -internal sealed partial class ConfigurationServiceEndPointResolver +internal sealed partial class ConfigurationServiceEndpointProvider { private sealed partial class Log { @@ -19,10 +19,10 @@ private sealed partial class Log [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(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); + [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); - internal static void ConfiguredEndPoints(ILogger logger, string serviceName, string path, IList endpoints, int added) + internal static void ConfiguredEndpoints(ILogger logger, string serviceName, string path, IList endpoints, int added) { if (!logger.IsEnabled(LogLevel.Debug)) { @@ -40,8 +40,8 @@ internal static void ConfiguredEndPoints(ILogger logger, string serviceName, str endpointValues.Append(endpoints[i].ToString()); } - var configuredEndPoints = endpointValues.ToString(); - ConfiguredEndPoints(logger, serviceName, path, configuredEndPoints); + var configuredEndpoints = endpointValues.ToString(); + ConfiguredEndpoints(logger, serviceName, path, configuredEndpoints); } [LoggerMessage(5, LogLevel.Debug, "No valid endpoint configuration was found for endpoint '{EndpointName}' on service '{ServiceName}' from path '{Path}'.", EventName = "EndpointConfigurationNotFound")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs similarity index 76% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 9604ec0c201..37078e01969 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -10,36 +10,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// A service endpoint resolver that uses configuration to resolve resolved. +/// A service endpoint provider that uses configuration to resolve resolved. /// -internal sealed partial class ConfigurationServiceEndPointResolver : IServiceEndPointProvider, IHostNameFeature +internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEndpointProvider, IHostNameFeature { - private const string DefaultEndPointName = "default"; + private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; private readonly string[] _schemes; private readonly IConfiguration _configuration; - private readonly ILogger _logger; - private readonly IOptions _options; + private readonly ILogger _logger; + private readonly IOptions _options; /// - /// Initializes a new instance. + /// Initializes a new instance. /// /// The query. /// The configuration. /// The logger. - /// Configuration resolver options. + /// Configuration provider options. /// Service discovery options. - public ConfigurationServiceEndPointResolver( - ServiceEndPointQuery query, + public ConfigurationServiceEndpointProvider( + ServiceEndpointQuery query, IConfiguration configuration, - ILogger logger, - IOptions options, + ILogger logger, + IOptions options, IOptions serviceDiscoveryOptions) { _serviceName = query.ServiceName; - _endpointName = query.EndPointName; - _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludeSchemes, serviceDiscoveryOptions.Value.AllowedSchemes); + _endpointName = query.EndpointName; + _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; _options = options; @@ -49,10 +49,10 @@ public ConfigurationServiceEndPointResolver( public ValueTask DisposeAsync() => default; /// - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + 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) + if (endpoints.Endpoints.Count != 0) { Log.SkippedResolution(_logger, _serviceName, "Collection has existing endpoints"); return default; @@ -62,12 +62,12 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo var section = _configuration.GetSection(_options.Value.SectionName).GetSection(_serviceName); if (!section.Exists()) { - endPoints.AddChangeToken(_configuration.GetReloadToken()); + endpoints.AddChangeToken(_configuration.GetReloadToken()); Log.ServiceConfigurationNotFound(_logger, _serviceName, $"{_options.Value.SectionName}:{_serviceName}"); return default; } - endPoints.AddChangeToken(section.GetReloadToken()); + endpoints.AddChangeToken(section.GetReloadToken()); // Find an appropriate configuration section based on the input. IConfigurationSection? namedSection = null; @@ -77,7 +77,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (_schemes.Length == 0) { // Use the section named "default". - endpointName = DefaultEndPointName; + endpointName = DefaultEndpointName; namedSection = section.GetSection(endpointName); } else @@ -112,14 +112,14 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo return default; } - List resolved = []; + List resolved = []; Log.UsingConfigurationPath(_logger, configPath, endpointName, _serviceName); // Account for both the single and multi-value cases. if (!string.IsNullOrWhiteSpace(namedSection.Value)) { // Single value case. - AddEndPoint(resolved, namedSection, endpointName); + AddEndpoint(resolved, namedSection, endpointName); } else { @@ -131,7 +131,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has non-numeric keys."); } - AddEndPoint(resolved, child, endpointName); + AddEndpoint(resolved, child, endpointName); } } @@ -158,13 +158,13 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo if (index >= 0 && index <= minIndex) { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } else { ++added; - endPoints.EndPoints.Add(ep); + endpoints.Endpoints.Add(ep); } } @@ -174,7 +174,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo } else { - Log.ConfiguredEndPoints(_logger, _serviceName, configPath, endPoints.EndPoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); } return default; @@ -182,7 +182,7 @@ public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationTo string IHostNameFeature.HostName => _serviceName; - private void AddEndPoint(List endPoints, IConfigurationSection section, string endpointName) + private void AddEndpoint(List endpoints, IConfigurationSection section, string endpointName) { var value = section.Value; if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint)) @@ -190,7 +190,7 @@ private void AddEndPoint(List endPoints, IConfigurationSection throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'."); } - endPoints.Add(CreateEndPoint(endPoint)); + endpoints.Add(CreateEndpoint(endPoint)); } private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint) @@ -220,16 +220,16 @@ private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPo return true; } - private ServiceEndPoint CreateEndPoint(EndPoint endPoint) + private ServiceEndpoint CreateEndpoint(EndPoint endPoint) { - var serviceEndPoint = ServiceEndPoint.Create(endPoint); - serviceEndPoint.Features.Set(this); - if (_options.Value.ApplyHostNameMetadata(serviceEndPoint)) + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (_options.Value.ShouldApplyHostNameMetadata(serviceEndpoint)) { - serviceEndPoint.Features.Set(this); + serviceEndpoint.Features.Set(this); } - return serviceEndPoint; + return serviceEndpoint; } public override string ToString() => "Configuration"; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs similarity index 58% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs index 032c50b6f27..a966cd44794 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderFactory.cs @@ -9,18 +9,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; /// -/// implementation that resolves services using . +/// implementation that resolves services using . /// -internal sealed class ConfigurationServiceEndPointResolverProvider( +internal sealed class ConfigurationServiceEndpointProviderFactory( IConfiguration configuration, - IOptions options, + IOptions options, IOptions serviceDiscoveryOptions, - ILogger logger) : IServiceEndPointProviderFactory + ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - resolver = new ConfigurationServiceEndPointResolver(query, configuration, logger, options, serviceDiscoveryOptions); + provider = new ConfigurationServiceEndpointProvider(query, configuration, logger, options, serviceDiscoveryOptions); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs similarity index 62% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs index 91e97b5d0bc..f8092c4dd51 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndPointResolverOptionsValidator.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProviderOptionsValidator.cs @@ -1,22 +1,22 @@ -// 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.Configuration; -internal sealed class ConfigurationServiceEndPointResolverOptionsValidator : IValidateOptions +internal sealed class ConfigurationServiceEndpointProviderOptionsValidator : IValidateOptions { - public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndPointResolverOptions options) + public ValidateOptionsResult Validate(string? name, ConfigurationServiceEndpointProviderOptions options) { if (string.IsNullOrWhiteSpace(options.SectionName)) { return ValidateOptionsResult.Fail($"{nameof(options.SectionName)} must not be null or empty."); } - if (options.ApplyHostNameMetadata is null) + if (options.ShouldApplyHostNameMetadata is null) { - return ValidateOptionsResult.Fail($"{nameof(options.ApplyHostNameMetadata)} must not be null."); + return ValidateOptionsResult.Fail($"{nameof(options.ShouldApplyHostNameMetadata)} must not be null."); } return ValidateOptionsResult.Success; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs index d3b94f2f1e7..29f28e359f7 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndPointResolverOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ConfigurationServiceEndpointProviderOptions.cs @@ -6,9 +6,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for . +/// Options for . /// -public sealed class ConfigurationServiceEndPointResolverOptions +public sealed class ConfigurationServiceEndpointProviderOptions { /// /// The name of the configuration section which contains service endpoints. Defaults to "Services". @@ -18,5 +18,5 @@ public sealed class ConfigurationServiceEndPointResolverOptions /// /// 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; + public Func ShouldApplyHostNameMetadata { get; set; } = _ => false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs similarity index 82% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index 44e58b0dbbb..e11f593776f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -11,13 +11,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// Resolves endpoints for HTTP requests. /// -internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory resolverFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable +internal sealed class HttpServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, IServiceProvider serviceProvider, TimeProvider timeProvider) : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((HttpServiceEndPointResolver)s!).CleanupResolvers(); + 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 ServiceEndPointWatcherFactory _resolverFactory = resolverFactory; + private readonly ServiceEndpointWatcherFactory _watcherFactory = watcherFactory; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; private Task? _cleanupTask; @@ -29,7 +29,7 @@ internal sealed class HttpServiceEndPointResolver(ServiceEndPointWatcherFactory /// A . /// The resolved service endpoint. /// The request had no set or a suitable endpoint could not be found. - public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) + public async ValueTask GetEndpointAsync(HttpRequestMessage request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(request); if (request.RequestUri is null) @@ -47,15 +47,15 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ static (name, self) => self.CreateResolver(name), this); - var (valid, endPoint) = await resolver.TryGetEndPointAsync(request, cancellationToken).ConfigureAwait(false); + var (valid, endpoint) = await resolver.TryGetEndpointAsync(request, cancellationToken).ConfigureAwait(false); if (valid) { - if (endPoint is null) + if (endpoint is null) { throw new InvalidOperationException($"Unable to resolve endpoint for service {resolver.ServiceName}"); } - return endPoint; + return endpoint; } } } @@ -148,37 +148,37 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverFactory.CreateWatcher(serviceName); - var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndPointSelector(); - var result = new ResolverEntry(resolver, selector); - resolver.Start(); + var watcher = _watcherFactory.CreateWatcher(serviceName); + var selector = serviceProvider.GetService() ?? new RoundRobinServiceEndpointSelector(); + var result = new ResolverEntry(watcher, selector); + watcher.Start(); return result; } private sealed class ResolverEntry : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver; - private readonly IServiceEndPointSelector _selector; + private readonly ServiceEndpointWatcher _watcher; + private readonly IServiceEndpointSelector _selector; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public ResolverEntry(ServiceEndPointWatcher resolver, IServiceEndPointSelector selector) + public ResolverEntry(ServiceEndpointWatcher watcher, IServiceEndpointSelector selector) { - _resolver = resolver; + _watcher = watcher; _selector = selector; - _resolver.OnEndPointsUpdated += result => + _watcher.OnEndpointsUpdated += result => { if (result.ResolvedSuccessfully) { - _selector.SetEndPoints(result.EndPointSource); + _selector.SetEndpoints(result.EndpointSource); } }; } - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -189,17 +189,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPoint? EndPoint)> TryGetEndPointAsync(object? context, CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpoint? Endpoint)> TryGetEndpointAsync(object? context, CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - var result = _selector.GetEndPoint(context); + await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + var result = _selector.GetEndpoint(context); return (true, result); } else @@ -246,7 +246,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs index 0febfa94815..0c5bd02d10d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/IServiceDiscoveryHttpMessageHandlerFactory.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. namespace Microsoft.Extensions.ServiceDiscovery.Http; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs index bc06a031700..a0063ae476b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpClientHandler.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// /// which resolves endpoints using service discovery. /// -internal sealed class ResolvingHttpClientHandler(HttpServiceEndPointResolver resolver, IOptions options) : HttpClientHandler +internal sealed class ResolvingHttpClientHandler(HttpServiceEndpointResolver resolver, IOptions options) : HttpClientHandler { - private readonly HttpServiceEndPointResolver _resolver = resolver; + private readonly HttpServiceEndpointResolver _resolver = resolver; private readonly ServiceDiscoveryOptions _options = options.Value; /// @@ -23,7 +23,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = ResolvingHttpDelegatingHandler.GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs index daa7b8a17de..8f13bb60ab5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ResolvingHttpDelegatingHandler.cs @@ -11,7 +11,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; /// internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler { - private readonly HttpServiceEndPointResolver _resolver; + private readonly HttpServiceEndpointResolver _resolver; private readonly ServiceDiscoveryOptions _options; /// @@ -19,7 +19,7 @@ internal sealed class ResolvingHttpDelegatingHandler : DelegatingHandler /// /// The endpoint resolver. /// The service discovery options. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options) { _resolver = resolver; _options = options.Value; @@ -31,7 +31,7 @@ public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOpt /// The endpoint resolver. /// The service discovery options. /// The inner handler. - public ResolvingHttpDelegatingHandler(HttpServiceEndPointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) + public ResolvingHttpDelegatingHandler(HttpServiceEndpointResolver resolver, IOptions options, HttpMessageHandler innerHandler) : base(innerHandler) { _resolver = resolver; _options = options.Value; @@ -44,7 +44,7 @@ protected override async Task SendAsync(HttpRequestMessage if (originalUri?.Host is not null) { var result = await _resolver.GetEndpointAsync(request, cancellationToken).ConfigureAwait(false); - request.RequestUri = GetUriWithEndPoint(originalUri, result, _options); + request.RequestUri = GetUriWithEndpoint(originalUri, result, _options); request.Headers.Host ??= result.Features.Get()?.HostName; } @@ -58,11 +58,11 @@ protected override async Task SendAsync(HttpRequestMessage } } - internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, ServiceDiscoveryOptions options) + internal static Uri GetUriWithEndpoint(Uri uri, ServiceEndpoint serviceEndpoint, ServiceDiscoveryOptions options) { - var endpoint = serviceEndPoint.EndPoint; + var endPoint = serviceEndpoint.EndPoint; UriBuilder result; - if (endpoint is UriEndPoint { Uri: { } ep }) + if (endPoint is UriEndPoint { Uri: { } ep }) { result = new UriBuilder(uri) { @@ -84,7 +84,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, { string host; int port; - switch (endpoint) + switch (endPoint) { case IPEndPoint ip: host = ip.Address.ToString(); @@ -95,7 +95,7 @@ internal static Uri GetUriWithEndPoint(Uri uri, ServiceEndPoint serviceEndPoint, port = dns.Port; break; default: - throw new InvalidOperationException($"Endpoints of type {endpoint.GetType()} are not supported"); + throw new InvalidOperationException($"Endpoints of type {endPoint.GetType()} are not supported"); } result = new UriBuilder(uri) @@ -112,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.AllowAllSchemes) || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + if (options.AllowAllSchemes || options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) { result.Scheme = scheme; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs index 0d3ba00122f..e5e7f7587bb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/ServiceDiscoveryHttpMessageHandlerFactory.cs @@ -8,12 +8,12 @@ namespace Microsoft.Extensions.ServiceDiscovery.Http; internal sealed class ServiceDiscoveryHttpMessageHandlerFactory( TimeProvider timeProvider, IServiceProvider serviceProvider, - ServiceEndPointWatcherFactory factory, + ServiceEndpointWatcherFactory factory, IOptions options) : IServiceDiscoveryHttpMessageHandlerFactory { public HttpMessageHandler CreateHandler(HttpMessageHandler handler) { - var registry = new HttpServiceEndPointResolver(factory, serviceProvider, timeProvider); + var registry = new HttpServiceEndpointResolver(factory, serviceProvider, timeProvider); return new ResolvingHttpDelegatingHandler(registry, options, handler); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs similarity index 73% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs index 07bffa5654b..675941bb955 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndPointResolverResult.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Internal/ServiceEndpointResolverResult.cs @@ -8,9 +8,9 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; /// /// Represents the result of service endpoint resolution. /// -/// The endpoint collection. +/// The endpoint collection. /// The exception which occurred during resolution. -internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPointSource, Exception? exception) +internal sealed class ServiceEndpointResolverResult(ServiceEndpointSource? endpointSource, Exception? exception) { /// /// Gets the exception which occurred during resolution. @@ -20,11 +20,11 @@ internal sealed class ServiceEndPointResolverResult(ServiceEndPointSource? endPo /// /// Gets a value indicating whether resolution completed successfully. /// - [MemberNotNullWhen(true, nameof(EndPointSource))] + [MemberNotNullWhen(true, nameof(EndpointSource))] public bool ResolvedSuccessfully => Exception is null; /// /// Gets the endpoints. /// - public ServiceEndPointSource? EndPointSource { get; } = endPointSource; + public ServiceEndpointSource? EndpointSource { get; } = endpointSource; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs similarity index 69% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs index bd0172c45cf..2d81ff38601 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/IServiceEndpointSelector.cs @@ -6,18 +6,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints from a collection of endpoints. /// -internal 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(ServiceEndPointSource endPoints); + /// The collection of endpoints to select from. + 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. - ServiceEndPoint GetEndPoint(object? context); + ServiceEndpoint GetEndpoint(object? context); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs similarity index 63% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs index 92da7cf25bf..e7e51bc6021 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndPointSelector.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/LoadBalancing/RoundRobinServiceEndpointSelector.cs @@ -6,21 +6,21 @@ namespace Microsoft.Extensions.ServiceDiscovery.LoadBalancing; /// /// Selects endpoints by iterating through the list of endpoints in a round-robin fashion. /// -internal sealed class RoundRobinServiceEndPointSelector : IServiceEndPointSelector +internal sealed class RoundRobinServiceEndpointSelector : IServiceEndpointSelector { private uint _next; - private IReadOnlyList? _endPoints; + private IReadOnlyList? _endpoints; /// - public void SetEndPoints(ServiceEndPointSource endPoints) + public void SetEndpoints(ServiceEndpointSource endpoints) { - _endPoints = endPoints.EndPoints; + _endpoints = endpoints.Endpoints; } /// - public ServiceEndPoint GetEndPoint(object? context) + public ServiceEndpoint GetEndpoint(object? context) { - if (_endPoints is not { Count: > 0 } collection) + if (_endpoints is not { Count: > 0 } collection) { throw new InvalidOperationException("The endpoint collection contains no endpoints"); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs similarity index 78% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index 570eb5e4e47..f9a984cfe4f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -5,11 +5,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; -internal sealed partial class PassThroughServiceEndPointResolver +internal sealed partial class PassThroughServiceEndpointProvider { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint resolver for service '{ServiceName}'.", EventName = "UsingPassThrough")] + [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs index 483c08702df..478d81d42dc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.cs @@ -7,18 +7,18 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver which passes through the provided value. +/// Service endpoint provider which passes through the provided value. /// -internal sealed partial class PassThroughServiceEndPointResolver(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndPointProvider +internal sealed partial class PassThroughServiceEndpointProvider(ILogger logger, string serviceName, EndPoint endPoint) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) { - if (endPoints.EndPoints.Count == 0) + if (endpoints.Endpoints.Count == 0) { Log.UsingPassThrough(logger, serviceName); - var ep = ServiceEndPoint.Create(endPoint); - ep.Features.Set(this); - endPoints.EndPoints.Add(ep); + var ep = ServiceEndpoint.Create(endPoint); + ep.Features.Set(this); + endpoints.Endpoints.Add(ep); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs similarity index 70% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs index 83455e0979c..2bf8c0cb481 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndPointResolverProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProviderFactory.cs @@ -8,29 +8,29 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; /// -/// Service endpoint resolver provider which passes through the provided value. +/// Service endpoint provider factory which creates pass-through providers. /// -internal sealed class PassThroughServiceEndPointResolverProvider(ILogger logger) : IServiceEndPointProviderFactory +internal sealed class PassThroughServiceEndpointProviderFactory(ILogger logger) : IServiceEndpointProviderFactory { /// - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - var serviceName = query.OriginalString; + var serviceName = query.ToString()!; if (!TryCreateEndPoint(serviceName, out var endPoint)) { // Propagate the value through regardless, leaving it to the caller to interpret it. endPoint = new DnsEndPoint(serviceName, 0); } - resolver = new PassThroughServiceEndPointResolver(logger, serviceName, endPoint); + provider = new PassThroughServiceEndpointProvider(logger, serviceName, endPoint); return true; } - private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? serviceEndPoint) + private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] out EndPoint? endPoint) { if ((serviceName.Contains("://", StringComparison.Ordinal) || !Uri.TryCreate($"fakescheme://{serviceName}", default, out var uri)) && !Uri.TryCreate(serviceName, default, out uri)) { - serviceEndPoint = null; + endPoint = null; return false; } @@ -50,15 +50,15 @@ private static bool TryCreateEndPoint(string serviceName, [NotNullWhen(true)] ou var port = uri.Port > 0 ? uri.Port : 0; if (IPAddress.TryParse(host, out var ip)) { - serviceEndPoint = new IPEndPoint(ip, port); + endPoint = new IPEndPoint(ip, port); } else if (!string.IsNullOrEmpty(host)) { - serviceEndPoint = new DnsEndPoint(host, port); + endPoint = new DnsEndPoint(host, port); } else { - serviceEndPoint = null; + endPoint = null; return false; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 8bece9644ff..04119540e10 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -6,29 +6,27 @@ In typical systems, service configuration changes over time. Service discovery a ## How it works -Service discovery uses configured _resolvers_ to resolve service endpoints. When service endpoints are resolved, each registered resolver is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndPointCollection`). +Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Resolvers implement the `IServiceEndPointResolver` interface. They are created by an instance of `IServiceEndPointResolverProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `UseServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. -Services can be resolved directly by calling `ServiceEndPointResolverRegistry`'s `GetEndPointsAsync` method, which returns a collection of resolved endpoints. +Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. ### Change notifications -Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndPointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). +Service configuration can change over time. Service discovery accounts for by monitoring endpoint configuration using push-based notifications where supported, falling back to polling in other cases. When endpoints are refreshed, callers are notified so that they can observe the refreshed results. To subscribe to notifications, callers use the `ChangeToken` property of `ServiceEndpointCollection`. For more information on change tokens, see [Detect changes with change tokens in ASP.NET Core](https://learn.microsoft.com/aspnet/core/fundamentals/change-tokens?view=aspnetcore-7.0). ### Extensibility using features -Service endpoints (`ServiceEndPoint` instances) and collections of service endpoints (`ServiceEndPointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by resolvers. Features which may be available on a `ServiceEndPoint` include: +Service endpoints (`ServiceEndpoint` instances) and collections of service endpoints (`ServiceEndpointCollection` instances) expose an extensible [`IFeatureCollection`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.http.features.ifeaturecollection) via their `Features` property. Features are exposed as interfaces accessible on the feature collection. These interfaces can be added, modified, wrapped, replaced or even removed at resolution time by providers. Features which may be available on a `ServiceEndpoint` include: * `IHostNameFeature`: exposes the host name of the resolved endpoint, intended for use with [Server Name Identification (SNI)](https://en.wikipedia.org/wiki/Server_Name_Indication) and [Transport Layer Security (TLS)](https://en.wikipedia.org/wiki/Transport_Layer_Security). -* `IEndPointHealthFeature`: used for reporting response times and errors from endpoints. -* `IEndPointLoadFeature`: used to query estimated endpoint load. ### Resolution order -The resolvers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. +The providers included in the `Microsoft.Extensions.ServiceDiscovery` series of packages skip resolution if there are existing endpoints in the collection when they are called. For example, consider a case where the following providers are registered: _Configuration_, _DNS SRV_, _Pass-through_. When resolution occurs, the providers will be called in-order. If the _Configuration_ providers discovers no endpoints, the _DNS SRV_ provider will perform resolution and may add one or more endpoints. If the _DNS SRV_ provider adds an endpoint to the collection, the _Pass-through_ provider will skip its resolution and will return immediately instead. ## Getting Started @@ -42,19 +40,19 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint resolvers. +In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); ``` -Add service discovery to an individual `IHttpClientBuilder` by calling the `UseServiceDiscovery` extension method: +Add service discovery to an individual `IHttpClientBuilder` by calling the `AddServiceDiscovery` extension method: ```csharp builder.Services.AddHttpClient(c => { c.BaseAddress = new("http://catalog")); -}).UseServiceDiscovery(); +}).AddServiceDiscovery(); ``` Alternatively, you can add service discovery to all `HttpClient` instances by default: @@ -63,14 +61,14 @@ Alternatively, you can add service discovery to all `HttpClient` instances by de builder.Services.ConfigureHttpClientDefaults(http => { // Turn on service discovery by default - http.UseServiceDiscovery(); + http.AddServiceDiscovery(); }); ``` ### Resolving service endpoints from configuration -The `AddServiceDiscovery` extension method adds a configuration-based endpoint resolver by default. -This resolver reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). +The `AddServiceDiscovery` extension method adds a configuration-based endpoint provider by default. +This provider reads endpoints from the [.NET Configuration system](https://learn.microsoft.com/dotnet/core/extensions/configuration). The library supports configuration through `appsettings.json`, environment variables, or any other `IConfiguration` source. Here is an example demonstrating how to configure a endpoints for the service named _catalog_ via `appsettings.json`: @@ -89,30 +87,30 @@ Here is an example demonstrating how to configure a endpoints for the service na The above example adds two endpoints for the service named _catalog_: `localhost:8080`, and `"10.46.24.90:80"`. Each time the _catalog_ is resolved, one of these endpoints will be selected. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint resolver can be added by calling the `AddConfigurationServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the configuration-based endpoint provider can be added by calling the `AddConfigurationServiceEndpointProvider` extension method on `IServiceCollection`. ### Configuration -The configuration resolver is configured using the `ConfigurationServiceEndPointResolverOptions` class, which offers these configuration options: +The configuration provider is configured using the `ConfigurationServiceEndpointProviderOptions` class, which offers these configuration options: * **`SectionName`**: The name of the configuration section that contains service endpoints. It defaults to `"Services"`. -* **`ApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. +* **`ShouldApplyHostNameMetadata`**: A delegate used to determine if host name metadata should be applied to resolved endpoints. It defaults to a function that returns `false`. To configure these options, you can use the `Configure` extension method on the `IServiceCollection` within your application's startup class or main program file: ```csharp var builder = WebApplication.CreateBuilder(args); -builder.Services.Configure(options => +builder.Services.Configure(options => { options.SectionName = "MyServiceEndpoints"; // Configure the logic for applying host name metadata - options.ApplyHostNameMetadata = endpoint => + options.ShouldApplyHostNameMetadata = endpoint => { // Your custom logic here. For example: - return endpoint.EndPoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); + return endpoint.Endpoint is DnsEndPoint dnsEp && dnsEp.Host.StartsWith("internal"); }; }); ``` @@ -121,38 +119,38 @@ This example demonstrates setting a custom section name for your service endpoin ## Resolving service endpoints using platform-provided service discovery -Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through resolver exists to support this scenario while still allowing other resolvers (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. +Some platforms, such as Azure Container Apps and Kubernetes (if configured), provide functionality for service discovery without the need for a service discovery client library. When an application is deployed to one of these environments, it may be preferable to use the platform's existing functionality instead. The pass-through provider exists to support this scenario while still allowing other provider (such as configuration) to be used in other environments, such as on the developer's machine, without requiring a code change or conditional guards. -The pass-through resolver performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. +The pass-through provider performs no external resolution and instead resolves endpoints by returning the input service name represented as a `DnsEndPoint`. The pass-through provider is configured by-default when adding service discovery via the `AddServiceDiscovery` extension method. -If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndPointResolver` extension method on `IServiceCollection`. +If service discovery was added to the host using the `AddServiceDiscoveryCore` extension method on `IServiceCollection`, the pass-through provider can be added by calling the `AddPassThroughServiceEndpointProvider` extension method on `IServiceCollection`. In the case of Azure Container Apps, the service name should match the app name. For example, if you have a service named "basket", then you should have a corresponding Azure Container App named "basket". ## Load-balancing with endpoint selectors -Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndPointSelector` instance to the `UseServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndPointSelector.Instance` as the endpoint selector: +Each time an endpoint is resolved by the `HttpClient` pipeline, a single endpoint will be selected from the set of all known endpoints for the requested service. If multiple endpoints are available, it may be desirable to balance traffic across all such endpoints. To accomplish this, a customizable _endpoint selector_ can be used. By default, endpoints are selected in round-robin order. To use a different endpoint selector, provide an `IServiceEndpointSelector` instance to the `AddServiceDiscovery` method call. For example, to select a random endpoint from the set of resolved endpoints, specify `RandomServiceEndpointSelector.Instance` as the endpoint selector: ```csharp builder.Services.AddHttpClient( static client => client.BaseAddress = new("http://catalog")); - .UseServiceDiscovery(RandomServiceEndPointSelector.Instance); + .AddServiceDiscovery(RandomServiceEndpointSelector.Instance); ``` The _Microsoft.Extensions.ServiceDiscovery_ package includes the following endpoint selector providers: -* Pick-first, which always selects the first endpoint: `PickFirstServiceEndPointSelectorProvider.Instance` -* Round-robin, which cycles through endpoints: `RoundRobinServiceEndPointSelectorProvider.Instance` -* Random, which selects endpoints randomly: `RandomServiceEndPointSelectorProvider.Instance` -* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndPointLoadFeature` feature: `PowerOfTwoChoicesServiceEndPointSelectorProvider.Instance` +* Pick-first, which always selects the first endpoint: `PickFirstServiceEndpointSelectorProvider.Instance` +* Round-robin, which cycles through endpoints: `RoundRobinServiceEndpointSelectorProvider.Instance` +* Random, which selects endpoints randomly: `RandomServiceEndpointSelectorProvider.Instance` +* Power-of-two-choices, which attempts to pick the least heavily loaded endpoint based on the _Power of Two Choices_ algorithm for distributed load balancing, degrading to randomly selecting an endpoint when either of the provided endpoints do not have the `IEndpointLoadFeature` feature: `PowerOfTwoChoicesServiceEndpointSelectorProvider.Instance` -Endpoint selectors are created via an `IServiceEndPointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndPointSelector`. The `IServiceEndPointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndPoints(ServiceEndPointCollection collection)` method. To choose an endpoint from the collection, the `GetEndPoint(object? context)` method is called, returning a single `ServiceEndPoint`. The `context` value passed to `GetEndPoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndPointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. +Endpoint selectors are created via an `IServiceEndpointSelectorProvider` instance, such as those listed above. The provider's `CreateSelector()` method is called to create a selector, which is an instance of `IServiceEndpointSelector`. The `IServiceEndpointSelector` instance is given the set of known endpoints when they are resolved, using the `SetEndpoints(ServiceEndpointCollection collection)` method. To choose an endpoint from the collection, the `GetEndpoint(object? context)` method is called, returning a single `ServiceEndpoint`. The `context` value passed to `GetEndpoint` is used to provide extra context which may be useful to the selector. For example, in the `HttpClient` case, the `HttpRequestMessage` is passed. None of the provided implementations of `IServiceEndpointSelector` inspect the context, and it can be ignored unless you are using a selector which does make use of it. ## Service discovery in .NET Aspire -.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint resolver_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. +.NET Aspire includes functionality for configuring the service discovery at development and testing time. This functionality works by providing configuration in the format expected by the _configuration-based endpoint provider_ described above from the .NET Aspire AppHost project to the individual service projects added to the application model. Configuration for service discovery is only added for services which are referenced by a given project. For example, consider the following AppHost program: @@ -184,7 +182,7 @@ In the above example, two `HttpClient`s are added: one for the core basket servi ### Named endpoints using configuration -With the configuration-based endpoint resolver, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": +With the configuration-based endpoint provider, named endpoints can be specified in configuration by prefixing the endpoint value with `_endpointName.`, where `endpointName` is the endpoint name. For example, consider this _appsettings.json_ configuration which defined a default endpoint (with no name) and an endpoint named "dashboard": ```json { @@ -199,7 +197,7 @@ With the configuration-based endpoint resolver, named endpoints can be specified ### Named endpoints in .NET Aspire -.NET Aspire uses the configuration-based resolver at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: +.NET Aspire uses the configuration-based provider at development and testing time, providing convenient APIs for configuring named endpoints which are then translated into configuration for the target services. For example: ```csharp var builder = DistributedApplication.CreateBuilder(args); @@ -208,13 +206,13 @@ var basket = builder.AddProject("basket") .WithEndpoint(hostPort: 9999, scheme: "http", name: "admin"); var adminDashboard = builder.AddProject("admin-dashboard") - .WithReference(basket.GetEndPoint("admin")); + .WithReference(basket.GetEndpoint("admin")); var frontend = builder.AddProject("frontend") .WithReference(basket); ``` -In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndPoint(string name)` method, as in the following example: +In the above example, the "basket" service exposes an "admin" endpoint in addition to the default "http" endpoint which it exposes. This endpoint is consumed by the "admin-dashboard" project, while the "frontend" project consumes all endpoints from "basket". Alternatively, the "frontend" project could be made to consume only the default "http" endpoint from "basket" by using the `GetEndpoint(string name)` method, as in the following example: ```csharp @@ -226,7 +224,7 @@ var frontend = builder.AddProject("frontend") ### Named endpoints in Kubernetes using DNS SRV -When deploying to Kubernetes, the DNS SRV service endpoint resolver can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". +When deploying to Kubernetes, the DNS SRV service endpoint provider can be used to resolve named endpoints. For example, the following resource definition will result in a DNS SRV record being created for an endpoint named "default" and an endpoint named "dashboard", both on the service named "basket". ```yml apiVersion: v1 @@ -244,11 +242,11 @@ spec: port: 8888 ``` -To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint resolver to the host builder as follows: +To configure a service to resolve the "dashboard" endpoint on the "basket" service, add the DNS SRV service endpoint provider to the host builder as follows: ```csharp builder.Services.AddServiceDiscoveryCore(); -builder.Services.AddDnsSrvServiceEndPointResolver(); +builder.Services.AddDnsSrvServiceEndpointProvider(); ``` The special port name "default" is used to specify the default endpoint, resolved using the URI `http://basket`. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index b4c34ccb7c5..8a137aad4f8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -26,8 +26,8 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt httpClientBuilder.AddHttpMessageHandler(services => { var timeProvider = services.GetService() ?? TimeProvider.System; - var resolverProvider = services.GetRequiredService(); - var registry = new HttpServiceEndPointResolver(resolverProvider, services, timeProvider); + var watcherFactory = services.GetRequiredService(); + var registry = new HttpServiceEndpointResolver(watcherFactory, services, timeProvider); var options = services.GetRequiredService>(); return new ResolvingHttpDelegatingHandler(registry, options); }); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 89c5a2d2eb0..02ce1af162b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -6,21 +6,19 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// -/// Options for service endpoint resolvers. +/// Options for service endpoint resolution. /// public sealed class ServiceDiscoveryOptions { /// - /// The value indicating that all endpoint schemes are allowed. + /// Gets or sets a value indicating whether all URI schemes for URIs resolved by the service discovery system are allowed. + /// If this value is , all URI schemes are allowed. + /// If this value is , only the schemes specified in are allowed. /// -#pragma warning disable IDE0300 // Simplify collection initialization -#pragma warning disable CA1825 // Avoid zero-length array allocations - public static readonly string[] AllowAllSchemes = new string[0]; -#pragma warning restore CA1825 // Avoid zero-length array allocations -#pragma warning restore IDE0300 // Simplify collection initialization + public bool AllowAllSchemes { get; set; } = true; /// - /// Gets or sets the period between polling attempts for resolvers which do not support refresh notifications via . + /// Gets or sets the period between polling attempts for providers which do not support refresh notifications via . /// public TimeSpan RefreshPeriod { get; set; } = TimeSpan.FromSeconds(60); @@ -28,14 +26,13 @@ public sealed class ServiceDiscoveryOptions /// 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. - /// Schemes are not case-sensitive. + /// When is , this property is ignored. /// - public string[] AllowedSchemes { get; set; } = AllowAllSchemes; + public IList AllowedSchemes { get; set; } = new List(); - internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IReadOnlyList allowedSchemes) + internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowedSchemes.Equals(AllowAllSchemes)) + if (allowAllSchemes) { if (schemes is string[] array) { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index 6403b214631..a5d789b7e4e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -26,8 +26,8 @@ public static class ServiceDiscoveryServiceCollectionExtensions public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { return services.AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -36,11 +36,11 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, Action configureOptions) { return services.AddServiceDiscoveryCore(configureOptions: configureOptions) - .AddConfigurationServiceEndPointResolver() - .AddPassThroughServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider() + .AddPassThroughServiceEndpointProvider(); } /// @@ -48,7 +48,7 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: null); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -56,16 +56,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. /// The delegate used to configure service discovery options. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); services.TryAddSingleton(_ => TimeProvider.System); - services.TryAddTransient(); - services.TryAddSingleton(); + services.TryAddTransient(); + services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(sp => new ServiceEndPointResolver(sp.GetRequiredService(), sp.GetRequiredService())); + services.TryAddSingleton(sp => new ServiceEndpointResolver(sp.GetRequiredService(), sp.GetRequiredService())); if (configureOptions is not null) { services.Configure(configureOptions); @@ -75,26 +75,26 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { - return services.AddConfigurationServiceEndPointResolver(configureOptions: null); + return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); } /// - /// Configures a service discovery endpoint resolver which uses to resolve endpoints. + /// Configures a service discovery endpoint provider which uses to resolve endpoints. /// /// The delegate used to configure the provider. /// The service collection. /// The service collection. - public static IServiceCollection AddConfigurationServiceEndPointResolver(this IServiceCollection services, Action? configureOptions) + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); - services.AddTransient, ConfigurationServiceEndPointResolverOptionsValidator>(); + services.AddSingleton(); + services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); if (configureOptions is not null) { services.Configure(configureOptions); @@ -104,14 +104,14 @@ public static IServiceCollection AddConfigurationServiceEndPointResolver(this IS } /// - /// Configures a service discovery endpoint resolver which passes through the input without performing resolution. + /// Configures a service discovery endpoint provider which passes through the input without performing resolution. /// /// The service collection. /// The service collection. - public static IServiceCollection AddPassThroughServiceEndPointResolver(this IServiceCollection services) + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { services.AddServiceDiscoveryCore(); - services.AddSingleton(); + services.AddSingleton(); return services; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs deleted file mode 100644 index 90f62ab0597..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.cs +++ /dev/null @@ -1,61 +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.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.ServiceDiscovery.PassThrough; - -namespace Microsoft.Extensions.ServiceDiscovery; - -/// -/// Creates service endpoint watchers. -/// -internal sealed partial class ServiceEndPointWatcherFactory( - IEnumerable resolvers, - ILogger resolverLogger, - IOptions options, - TimeProvider timeProvider) -{ - 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; - - /// - /// Creates a service endpoint resolver for the provided service name. - /// - 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.TryCreateProvider(query, out var resolver)) - { - resolvers ??= []; - resolvers.Add(resolver); - } - } - - if (resolvers is not { Count: > 0 }) - { - throw new InvalidOperationException($"No resolver which supports the provided service name, '{serviceName}', has been configured."); - } - - Log.CreatingResolver(_logger, serviceName, resolvers); - return new ServiceEndPointWatcher( - resolvers: [.. resolvers], - logger: _logger, - serviceName: serviceName, - timeProvider: _timeProvider, - options: _options); - } -} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs index 1a14cb961b7..947f24b2f81 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointBuilder.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointBuilder.cs @@ -9,9 +9,9 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// A mutable collection of service endpoints. /// -internal sealed class ServiceEndPointBuilder : IServiceEndPointBuilder +internal sealed class ServiceEndpointBuilder : IServiceEndpointBuilder { - private readonly List _endPoints = new(); + private readonly List _endpoints = new(); private readonly List _changeTokens = new(); private readonly FeatureCollection _features = new FeatureCollection(); @@ -32,15 +32,15 @@ public void AddChangeToken(IChangeToken changeToken) /// /// Gets the endpoints. /// - public IList EndPoints => _endPoints; + public IList Endpoints => _endpoints; /// - /// Creates a from the provided instance. + /// Creates a from the provided instance. /// /// The service endpoint source. - public ServiceEndPointSource Build() + public ServiceEndpointSource Build() { - return new ServiceEndPointSource(_endPoints, new CompositeChangeToken(_changeTokens), _features); + return new ServiceEndpointSource(_endpoints, new CompositeChangeToken(_changeTokens), _features); } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs similarity index 84% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 029d2601243..92df120940d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -9,13 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Resolves service names to collections of endpoints. /// -public sealed class ServiceEndPointResolver : IAsyncDisposable +public sealed class ServiceEndpointResolver : IAsyncDisposable { - private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndPointResolver)s!).CleanupResolvers(); + private static readonly TimerCallback s_cleanupCallback = s => ((ServiceEndpointResolver)s!).CleanupResolvers(); private static readonly TimeSpan s_cleanupPeriod = TimeSpan.FromSeconds(10); private readonly object _lock = new(); - private readonly ServiceEndPointWatcherFactory _resolverProvider; + private readonly ServiceEndpointWatcherFactory _watcherFactory; private readonly TimeProvider _timeProvider; private readonly ConcurrentDictionary _resolvers = new(); private ITimer? _cleanupTimer; @@ -23,13 +23,13 @@ public sealed class ServiceEndPointResolver : IAsyncDisposable private bool _disposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The resolver factory. + /// The watcher factory. /// The time provider. - internal ServiceEndPointResolver(ServiceEndPointWatcherFactory resolverProvider, TimeProvider timeProvider) + internal ServiceEndpointResolver(ServiceEndpointWatcherFactory watcherFactory, TimeProvider timeProvider) { - _resolverProvider = resolverProvider; + _watcherFactory = watcherFactory; _timeProvider = timeProvider; } @@ -39,7 +39,7 @@ internal ServiceEndPointResolver(ServiceEndPointWatcherFactory 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); @@ -54,7 +54,7 @@ public async ValueTask GetEndPointsAsync(string serviceNa static (name, self) => self.CreateResolver(name), this); - var (valid, result) = await resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); + var (valid, result) = await resolver.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); if (valid) { if (result is null) @@ -156,21 +156,21 @@ private async Task CleanupResolversAsyncCore() private ResolverEntry CreateResolver(string serviceName) { - var resolver = _resolverProvider.CreateWatcher(serviceName); + var resolver = _watcherFactory.CreateWatcher(serviceName); resolver.Start(); return new ResolverEntry(resolver); } - private sealed class ResolverEntry(ServiceEndPointWatcher resolver) : IAsyncDisposable + private sealed class ResolverEntry(ServiceEndpointWatcher watcher) : IAsyncDisposable { - private readonly ServiceEndPointWatcher _resolver = resolver; + private readonly ServiceEndpointWatcher _watcher = watcher; private const ulong CountMask = ~(RecentUseFlag | DisposingFlag); private const ulong RecentUseFlag = 1UL << 62; private const ulong DisposingFlag = 1UL << 63; private ulong _status; private TaskCompletionSource? _onDisposed; - public string ServiceName => _resolver.ServiceName; + public string ServiceName => _watcher.ServiceName; public bool CanExpire() { @@ -181,17 +181,17 @@ public bool CanExpire() return (status & (CountMask | RecentUseFlag)) == 0; } - public async ValueTask<(bool Valid, ServiceEndPointSource? EndPoints)> GetEndPointsAsync(CancellationToken cancellationToken) + public async ValueTask<(bool Valid, ServiceEndpointSource? Endpoints)> GetEndpointsAsync(CancellationToken cancellationToken) { try { var status = Interlocked.Increment(ref _status); if ((status & DisposingFlag) == 0) { - // If the resolver is valid, resolve. + // If the watcher is valid, resolve. // We ensure that it will not be disposed while we are resolving. - var endPoints = await _resolver.GetEndPointsAsync(cancellationToken).ConfigureAwait(false); - return (true, endPoints); + var endpoints = await _watcher.GetEndpointsAsync(cancellationToken).ConfigureAwait(false); + return (true, endpoints); } else { @@ -237,7 +237,7 @@ private async Task DisposeAsyncCore() { try { - await _resolver.DisposeAsync().ConfigureAwait(false); + await _watcher.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs similarity index 60% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index 78a8f84b556..fce9f667b40 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -5,35 +5,35 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcher +partial class ServiceEndpointWatcher { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndPoints")] - public static partial void ResolvingEndPoints(ILogger logger, string serviceName); + [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] + public static partial void ResolvingEndpoints(ILogger logger, string serviceName); [LoggerMessage(2, LogLevel.Debug, "Endpoint resolution is pending for service '{ServiceName}'.", EventName = "ResolutionPending")] public static partial void ResolutionPending(ILogger logger, string serviceName); - [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); + [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, ServiceEndPointSource endPointSource) + public static void ResolutionSucceeded(ILogger logger, string serviceName, ServiceEndpointSource endpointSource) { if (logger.IsEnabled(LogLevel.Debug)) { - ResolutionSucceededCore(logger, endPointSource.EndPoints.Count, serviceName, string.Join(", ", endPointSource.EndPoints.Select(GetEndPointString))); + ResolutionSucceededCore(logger, endpointSource.Endpoints.Count, serviceName, string.Join(", ", endpointSource.Endpoints.Select(GetEndpointString))); } - static string GetEndPointString(ServiceEndPoint ep) + static string GetEndpointString(ServiceEndpoint ep) { - if (ep.Features.Get() is { } resolver) + if (ep.Features.Get() is { } provider) { - return $"{ep.GetEndPointString()} ({resolver})"; + return $"{ep} ({provider})"; } - return ep.GetEndPointString(); - } + return ep.ToString()!; + } } [LoggerMessage(4, LogLevel.Error, "Error resolving endpoints for service '{ServiceName}'.", EventName = "ResolutionFailed")] diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs similarity index 75% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index 9b1069d31e7..ba6df9b43c4 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -13,23 +13,23 @@ namespace Microsoft.Extensions.ServiceDiscovery; /// /// Watches for updates to the collection of resolved endpoints for a specified service. /// -internal sealed partial class ServiceEndPointWatcher( - IServiceEndPointProvider[] resolvers, +internal sealed partial class ServiceEndpointWatcher( + IServiceEndpointProvider[] providers, ILogger logger, string serviceName, TimeProvider timeProvider, IOptions options) : IAsyncDisposable { - private static readonly TimerCallback s_pollingAction = static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: true); + 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 ServiceDiscoveryOptions _options = options.Value; - private readonly IServiceEndPointProvider[] _resolvers = resolvers; + private readonly IServiceEndpointProvider[] _providers = providers; private readonly CancellationTokenSource _disposalCancellation = new(); private ITimer? _pollingTimer; - private ServiceEndPointSource? _cachedEndPoints; + private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; @@ -41,14 +41,14 @@ internal sealed partial class ServiceEndPointWatcher( /// /// Gets or sets the action called when endpoints are updated. /// - public Action? OnEndPointsUpdated { get; set; } + public Action? OnEndpointsUpdated { get; set; } /// /// Starts the endpoint resolver. /// public void Start() { - ThrowIfNoResolvers(); + ThrowIfNoProviders(); _ = RefreshAsync(force: false); } @@ -57,27 +57,27 @@ 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(); + ThrowIfNoProviders(); // If the cache is valid, return the cached value. - if (_cachedEndPoints is { ChangeToken.HasChanged: false } cached) + 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); + return GetEndpointsInternal(cancellationToken); - async ValueTask GetEndPointsInternal(CancellationToken cancellationToken) + async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { - ServiceEndPointSource? result; + ServiceEndpointSource? result; do { await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); - result = _cachedEndPoints; + result = _cachedEndpoints; } while (result is null); return result; } @@ -89,7 +89,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndPoints is null or { ChangeToken.HasChanged: true } || force)) + if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -124,27 +124,27 @@ private async Task RefreshAsyncInternal() await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); var cancellationToken = _disposalCancellation.Token; Exception? error = null; - ServiceEndPointSource? newEndPoints = null; + ServiceEndpointSource? newEndpoints = null; CacheStatus newCacheState; try { - Log.ResolvingEndPoints(_logger, ServiceName); - var builder = new ServiceEndPointBuilder(); - foreach (var resolver in _resolvers) + Log.ResolvingEndpoints(_logger, ServiceName); + var builder = new ServiceEndpointBuilder(); + foreach (var provider in _providers) { - await resolver.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); + await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } - var endPoints = builder.Build(); + 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 (endpoints.ChangeToken.ActiveChangeCallbacks) { // Initiate a background refresh, if necessary. - endPoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndPointWatcher)state!).RefreshAsync(force: false), this); + endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); if (_pollingTimer is { } timer) { _pollingTimer = null; @@ -157,7 +157,7 @@ private async Task RefreshAsyncInternal() } // The cache is valid - newEndPoints = endPoints; + newEndpoints = endpoints; newCacheState = CacheStatus.Valid; } } @@ -171,26 +171,26 @@ private async Task RefreshAsyncInternal() // If there was an error, the cache must be invalid. Debug.Assert(error is null || newCacheState is CacheStatus.Invalid); - // To ensure coherence between the value returned by calls made to GetEndPointsAsync and value passed to the callback, + // To ensure coherence between the value returned by calls made to GetEndpointsAsync and value passed to the callback, // we invalidate the cache before invoking the callback. This causes callers to wait on the refresh task - // before receiving the updated value. An alternative approach is to lock access to _cachedEndPoints, but + // before receiving the updated value. An alternative approach is to lock access to _cachedEndpoints, but // that will have more overhead in the common case. if (newCacheState is CacheStatus.Valid) { - Interlocked.Exchange(ref _cachedEndPoints, null); + Interlocked.Exchange(ref _cachedEndpoints, null); } - if (OnEndPointsUpdated is { } callback) + if (OnEndpointsUpdated is { } callback) { - callback(new(newEndPoints, error)); + callback(new(newEndpoints, error)); } lock (_lock) { if (newCacheState is CacheStatus.Valid) { - Debug.Assert(newEndPoints is not null); - _cachedEndPoints = newEndPoints; + Debug.Assert(newEndpoints is not null); + _cachedEndpoints = newEndpoints; } _cacheState = newCacheState; @@ -201,9 +201,9 @@ private async Task RefreshAsyncInternal() Log.ResolutionFailed(_logger, error, ServiceName); ExceptionDispatchInfo.Throw(error); } - else if (newEndPoints is not null) + else if (newEndpoints is not null) { - Log.ResolutionSucceeded(_logger, ServiceName, newEndPoints); + Log.ResolutionSucceeded(_logger, ServiceName, newEndpoints); } } @@ -240,9 +240,9 @@ public async ValueTask DisposeAsync() await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); } - foreach (var resolver in _resolvers) + foreach (var provider in _providers) { - await resolver.DisposeAsync().ConfigureAwait(false); + await provider.DisposeAsync().ConfigureAwait(false); } } @@ -253,14 +253,14 @@ private enum CacheStatus Valid } - private void ThrowIfNoResolvers() + private void ThrowIfNoProviders() { - if (_resolvers.Length == 0) + if (_providers.Length == 0) { - ThrowNoResolversConfigured(); + ThrowNoProvidersConfigured(); } } [DoesNotReturn] - private static void ThrowNoResolversConfigured() => throw new InvalidOperationException("No service endpoint resolvers are configured."); + private static void ThrowNoProvidersConfigured() => throw new InvalidOperationException("No service endpoint providers are configured."); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs similarity index 53% rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 69f565eb8e3..5f4acc89874 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndPointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -5,17 +5,18 @@ namespace Microsoft.Extensions.ServiceDiscovery; -partial class ServiceEndPointWatcherFactory +partial class ServiceEndpointWatcherFactory { private sealed partial class Log { - [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} resolvers: {Resolvers}.", EventName = "CreatingResolver")] - public static partial void ServiceEndPointResolverListCore(ILogger logger, string serviceName, int count, string resolvers); - public static void CreatingResolver(ILogger logger, string serviceName, List resolvers) + [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] + public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); + + public static void CreatingResolver(ILogger logger, string serviceName, List providers) { if (logger.IsEnabled(LogLevel.Debug)) { - ServiceEndPointResolverListCore(logger, serviceName, resolvers.Count, string.Join(", ", resolvers.Select(static r => r.ToString()))); + ServiceEndpointProviderListCore(logger, serviceName, providers.Count, string.Join(", ", providers.Select(static r => r.ToString()))); } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs new file mode 100644 index 00000000000..6cc7cb2cbc5 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.cs @@ -0,0 +1,61 @@ +// 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.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.PassThrough; + +namespace Microsoft.Extensions.ServiceDiscovery; + +/// +/// Creates service endpoint watchers. +/// +internal sealed partial class ServiceEndpointWatcherFactory( + IEnumerable providerFactories, + ILogger logger, + IOptions options, + TimeProvider timeProvider) +{ + private readonly IServiceEndpointProviderFactory[] _providerFactories = providerFactories + .Where(r => r is not PassThroughServiceEndpointProviderFactory) + .Concat(providerFactories.Where(static r => r is PassThroughServiceEndpointProviderFactory)).ToArray(); + private readonly ILogger _logger = logger; + private readonly TimeProvider _timeProvider = timeProvider; + private readonly IOptions _options = options; + + /// + /// Creates a service endpoint watcher for the provided service name. + /// + 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? providers = null; + foreach (var factory in _providerFactories) + { + if (factory.TryCreateProvider(query, out var provider)) + { + providers ??= []; + providers.Add(provider); + } + } + + if (providers is not { Count: > 0 }) + { + throw new InvalidOperationException($"No provider which supports the provided service name, '{serviceName}', has been configured."); + } + + Log.CreatingResolver(_logger, serviceName, providers); + return new ServiceEndpointWatcher( + providers: [.. providers], + logger: _logger, + serviceName: serviceName, + timeProvider: _timeProvider, + options: _options); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs similarity index 72% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 25cd88a1436..7cadb4e3c7f 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -14,10 +14,10 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// -/// Tests for and . -/// These also cover and by extension. +/// Tests for and . +/// These also cover and by extension. /// -public class DnsSrvServiceEndPointResolverTests +public class DnsSrvServiceEndpointResolverTests { private sealed class FakeDnsClient : IDnsQuery { @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndPoint_Dns() + public async Task ResolveServiceEndpoint_Dns() { var dnsClientMock = new FakeDnsClient { @@ -101,26 +101,26 @@ public async Task ResolveServiceEndPoint_Dns() var services = new ServiceCollection() .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns") + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -134,7 +134,7 @@ public async Task ResolveServiceEndPoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -175,28 +175,28 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { serviceCollection - .AddDnsSrvServiceEndPointResolver(options => + .AddDnsSrvServiceEndpointProvider(options => { options.QuerySuffix = ".ns"; - options.ApplyHostNameMetadata = _ => true; + options.ShouldApplyHostNameMetadata = _ => true; }) - .AddConfigurationServiceEndPointResolver(); + .AddConfigurationServiceEndpointProvider(); } else { serviceCollection - .AddConfigurationServiceEndPointResolver() - .AddDnsSrvServiceEndPointResolver(options => options.QuerySuffix = ".ns"); + .AddConfigurationServiceEndpointProvider() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns"); }; var services = serviceCollection.BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); @@ -205,13 +205,13 @@ public async Task ResolveServiceEndPoint_Dns_MultipleProviders_PreventMixing(boo if (dnsFirst) { // We expect only the results from the DNS provider. - Assert.Equal(3, initialResult.EndPointSource.EndPoints.Count); - var eps = initialResult.EndPointSource.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -221,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.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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -248,59 +248,4 @@ public void SetValues(IEnumerable> values) OnReload(); } } - - /* - [Fact] - public async Task ResolveServiceEndPoint_Dns_RespectsChangeToken() - { - var oneEndPoint = new Dictionary - { - ["services:basket:http:0:host"] = "localhost", - ["services:basket:http:0:port"] = "8080", - }; - var bothEndPoints = new Dictionary(oneEndPoint) - { - ["services:basket:http:1:host"] = "remotehost", - ["services:basket:http:1:port"] = "9090", - }; - var configSource = new MyConfigurationProvider(); - var services = new ServiceCollection() - .AddSingleton(new ConfigurationBuilder().Add(configSource).Build()) - .AddServiceDiscovery() - .AddConfigurationServiceEndPointResolver() - .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointResolver resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) - { - Assert.NotNull(resolver); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = v => channel.Writer.TryWrite(v); - resolver.Start(); - var initialResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(initialResult); - Assert.False(initialResult.ResolvedSuccessfully); - Assert.Equal(ResolutionStatusCode.Error, initialResult.Status.StatusCode); - Assert.Null(initialResult.EndPoints); - - // Update the config and check that it flows through the system. - configSource.SetValues(oneEndPoint); - - // If we don't get an update relatively soon, something is broken. We add a timeout here because we don't want an issue to - // cause an indefinite test hang. We expect the result to be published practically immediately, though. - _ = await channel.Reader.ReadAsync(CancellationToken.None).AsTask().WaitAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); - var oneEpResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); - var firstEp = Assert.Single(oneEpResult); - Assert.Equal(new DnsEndPoint("localhost", 8080), firstEp.EndPoint); - - // Do it again to check that an updated (not cached) version is published. - configSource.SetValues(bothEndPoints); - var twoEpResult = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); - Assert.True(twoEpResult.ResolvedSuccessfully); - Assert.Equal(2, twoEpResult.EndPoints.Count); - Assert.Equal(new DnsEndPoint("localhost", 8080), twoEpResult.EndPoints[0].EndPoint); - Assert.Equal(new DnsEndPoint("remotehost", 9090), twoEpResult.EndPoints[1].EndPoint); - } - } - */ } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6d8091f026c..db720782107 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -12,13 +12,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class ConfigurationServiceEndPointResolverTests +public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -27,23 +27,23 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("localhost", 8080), ep.EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -52,7 +52,7 @@ public async Task ResolveServiceEndPoint_Configuration_SingleResult() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() + public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() { // Try to resolve an http endpoint when only https is allowed. var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary @@ -63,59 +63,63 @@ public async Task ResolveServiceEndPoint_Configuration_DisallowedScheme() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() - .Configure(o => o.AllowedSchemes = ["https"]) + .AddConfigurationServiceEndpointProvider() + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; // Explicitly specifying http. // We should get no endpoint back because http is not allowed by configuration. - await using ((resolver = resolverFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Empty(initialResult.EndPointSource.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.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.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.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleResults() + public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { var configSource = new MemoryConfigurationSource { @@ -129,24 +133,24 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver(options => options.ApplyHostNameMetadata = _ => true) + .AddConfigurationServiceEndpointProvider(options => options.ShouldApplyHostNameMetadata = _ => true) .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -155,20 +159,20 @@ 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.CreateWatcher("https+http://basket")).ConfigureAwait(false)) + await using ((watcher = watcherFactory.CreateWatcher("https+http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.NotNull(hostNameFeature); @@ -178,7 +182,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleResults() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() { var configSource = new MemoryConfigurationSource { @@ -196,25 +200,25 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - 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.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.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); @@ -223,7 +227,7 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols() } [Fact] - public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() + public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpecificationByConsumer() { var configSource = new MemoryConfigurationSource { @@ -241,29 +245,29 @@ public async Task ResolveServiceEndPoint_Configuration_MultipleProtocols_WithSpe var services = new ServiceCollection() .AddSingleton(config.Build()) .AddServiceDiscoveryCore() - .AddConfigurationServiceEndPointResolver() + .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("https+http://_grpc.basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(3, initialResult.EndPointSource.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.EndPointSource.EndPoints[0].EndPoint); - Assert.Equal(new IPEndPoint(IPAddress.Loopback, 3333), initialResult.EndPointSource.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.EndPointSource.EndPoints[2].EndPoint); + Assert.Equal(new UriEndPoint(new Uri("https://remotehost:5555")), initialResult.EndpointSource.Endpoints[2].EndPoint); - Assert.All(initialResult.EndPointSource.EndPoints, ep => + Assert.All(initialResult.EndpointSource.Endpoints, ep => { var hostNameFeature = ep.Features.Get(); Assert.Null(hostNameFeature); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs similarity index 60% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index 643bbfad441..e0af5c03ed4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -12,36 +12,36 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for . -/// These also cover and by extension. +/// Tests for . +/// These also cover and by extension. /// -public class PassThroughServiceEndPointResolverTests +public class PassThroughServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndPoint_PassThrough() + public async Task ResolveServiceEndpoint_PassThrough() { var services = new ServiceCollection() .AddServiceDiscoveryCore() - .AddPassThroughServiceEndPointResolver() + .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - var ep = Assert.Single(initialResult.EndPointSource.EndPoints); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new DnsEndPoint("basket", 80), ep.EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Superseded() + public async Task ResolveServiceEndpoint_Superseded() { var configSource = new MemoryConfigurationSource { @@ -55,26 +55,26 @@ public async Task ResolveServiceEndPoint_Superseded() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the basket service to be resolved from Configuration, not the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("http://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } [Fact] - public async Task ResolveServiceEndPoint_Fallback() + public async Task ResolveServiceEndpoint_Fallback() { var configSource = new MemoryConfigurationSource { @@ -88,27 +88,27 @@ public async Task ResolveServiceEndPoint_Fallback() .AddSingleton(config.Build()) .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://catalog")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; - resolver.Start(); + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); var initialResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); // We expect the CATALOG service to be resolved from the pass-through provider. - Assert.Single(initialResult.EndPointSource.EndPoints); - Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndPointSource.EndPoints[0].EndPoint); + Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new DnsEndPoint("catalog", 80), initialResult.EndpointSource.Endpoints[0].EndPoint); } } // Ensures that pass-through resolution succeeds in scenarios where no scheme is specified during resolution. [Fact] - public async Task ResolveServiceEndPoint_Fallback_NoScheme() + public async Task ResolveServiceEndpoint_Fallback_NoScheme() { var configSource = new MemoryConfigurationSource { @@ -123,8 +123,8 @@ public async Task ResolveServiceEndPoint_Fallback_NoScheme() .AddServiceDiscovery() // Adds the configuration and pass-through providers. .BuildServiceProvider(); - var resolver = services.GetRequiredService(); - var result = await resolver.GetEndPointsAsync("catalog", default); - Assert.Equal(new DnsEndPoint("catalog", 0), result.EndPoints[0].EndPoint); + var resolver = services.GetRequiredService(); + var result = await resolver.GetEndpointsAsync("catalog", default); + Assert.Equal(new DnsEndPoint("catalog", 0), result.Endpoints[0].EndPoint); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs similarity index 62% rename from test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs rename to test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index f5e506a9b72..16950e67374 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndPointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -15,58 +15,58 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; /// -/// Tests for and . +/// Tests for and . /// -public class ServiceEndPointResolverTests +public class ServiceEndpointResolverTests { [Fact] - public void ResolveServiceEndPoint_NoResolversConfigured_Throws() + public void ResolveServiceEndpoint_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + 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); + Assert.Equal("No provider which supports the provided service name, 'https://basket', has been configured.", exception.Message); } [Fact] - public async Task ServiceEndPointResolver_NoResolversConfigured_Throws() + public async Task ServiceEndpointResolver_NoProvidersConfigured_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - 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()); - Assert.Equal("No service endpoint resolvers are configured.", exception.Message); + var watcher = new ServiceEndpointWatcher([], NullLogger.Instance, "foo", TimeProvider.System, Options.Options.Create(new ServiceDiscoveryOptions())); + var exception = Assert.Throws(watcher.Start); + Assert.Equal("No service endpoint providers are configured.", exception.Message); + exception = await Assert.ThrowsAsync(async () => await watcher.GetEndpointsAsync()); + Assert.Equal("No service endpoint providers are configured.", exception.Message); } [Fact] - public void ResolveServiceEndPoint_NullServiceName_Throws() + public void ResolveServiceEndpoint_NullServiceName_Throws() { var services = new ServiceCollection() .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var resolverFactory = services.GetRequiredService(); Assert.Throws(() => resolverFactory.CreateWatcher(null!)); } [Fact] - public async Task AddServiceDiscovery_NoResolvers_Throws() + public async Task AddServiceDiscovery_NoProviders_Throws() { var serviceCollection = new ServiceCollection(); 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); + Assert.Equal("No provider which supports the provided service name, 'http://foo', has been configured.", exception.Message); } - private sealed class FakeEndPointResolverProvider(Func createResolverDelegate) : IServiceEndPointProviderFactory + private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { - public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] out IServiceEndPointProvider? resolver) + public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) { bool result; (result, resolver) = createResolverDelegate(query); @@ -74,58 +74,58 @@ public bool TryCreateProvider(ServiceEndPointQuery query, [NotNullWhen(true)] ou } } - private sealed class FakeEndPointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndPointProvider + private sealed class FakeEndpointResolver(Func resolveAsync, Func disposeAsync) : IServiceEndpointProvider { - public ValueTask PopulateAsync(IServiceEndPointBuilder endPoints, CancellationToken cancellationToken) => resolveAsync(endPoints, cancellationToken); + public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationToken cancellationToken) => resolveAsync(endpoints, cancellationToken); public ValueTask DisposeAsync() => disposeAsync(); } [Fact] - public async Task ResolveServiceEndPoint() + public async Task ResolveServiceEndpoint() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + 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); - var tcs = new TaskCompletionSource(); - resolver.OnEndPointsUpdated = tcs.SetResult; + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); var resolverResult = await tcs.Task.ConfigureAwait(false); Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); - Assert.Equal(2, resolverResult.EndPointSource.EndPoints.Count); - var endpoints = resolverResult.EndPointSource.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]); @@ -133,34 +133,34 @@ public async Task ResolveServiceEndPoint() } [Fact] - public async Task ResolveServiceEndPointOneShot() + public async Task ResolveServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolver = services.GetRequiredService(); + var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndPointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); Assert.NotNull(initialResult); - var sep = Assert.Single(initialResult.EndPoints); + 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); @@ -169,36 +169,36 @@ public async Task ResolveServiceEndPointOneShot() } [Fact] - public async Task ResolveHttpServiceEndPointOneShot() + public async Task ResolveHttpServiceEndpointOneShot() { var cts = new[] { new CancellationTokenSource() }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: (collection, ct) => { collection.AddChangeToken(new CancellationChangeToken(cts[0].Token)); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); if (cts[0].Token.IsCancellationRequested) { cts[0] = new(); - collection.EndPoints.Add(ServiceEndPoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.2"), 8888))); } return default; }, disposeAsync: () => default); - var fakeResolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var fakeResolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(fakeResolverProvider) + .AddSingleton(fakeResolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverProvider = services.GetRequiredService(); - await using var resolver = new HttpServiceEndPointResolver(resolverProvider, services, 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"); - var endPoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); - Assert.NotNull(endPoint); - var ip = Assert.IsType(endPoint.EndPoint); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(endpoint); + var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); @@ -206,12 +206,12 @@ public async Task ResolveHttpServiceEndPointOneShot() } [Fact] - public async Task ResolveServiceEndPoint_ThrowOnReload() + public async Task ResolveServiceEndpoint_ThrowOnReload() { var sem = new SemaphoreSlim(0); var cts = new[] { new CancellationTokenSource() }; var throwOnNextResolve = new[] { false }; - var innerResolver = new FakeEndPointResolver( + var innerResolver = new FakeEndpointResolver( resolveAsync: async (collection, ct) => { await sem.WaitAsync(ct).ConfigureAwait(false); @@ -228,25 +228,25 @@ 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))); + collection.Endpoints.Add(ServiceEndpoint.Create(new IPEndPoint(IPAddress.Parse("127.1.1.1"), 8080))); }, disposeAsync: () => default); - var resolverProvider = new FakeEndPointResolverProvider(name => (true, innerResolver)); + var resolverProvider = new FakeEndpointResolverProvider(name => (true, innerResolver)); var services = new ServiceCollection() - .AddSingleton(resolverProvider) + .AddSingleton(resolverProvider) .AddServiceDiscoveryCore() .BuildServiceProvider(); - var resolverFactory = services.GetRequiredService(); + var watcherFactory = services.GetRequiredService(); - ServiceEndPointWatcher resolver; - await using ((resolver = resolverFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) + ServiceEndpointWatcher watcher; + await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { - Assert.NotNull(resolver); - var initialEndPointsTask = resolver.GetEndPointsAsync(CancellationToken.None).ConfigureAwait(false); + Assert.NotNull(watcher); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); sem.Release(1); - var initialEndPoints = await initialEndPointsTask; - Assert.NotNull(initialEndPoints); - Assert.Single(initialEndPoints.EndPoints); + var initialEndpoints = await initialEndpointsTask; + Assert.NotNull(initialEndpoints); + Assert.Single(initialEndpoints.Endpoints); // Tell the resolver to throw on the next resolve call and then trigger a reload. throwOnNextResolve[0] = true; @@ -254,21 +254,21 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() var exception = await Assert.ThrowsAsync(async () => { - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); }).ConfigureAwait(false); Assert.Equal("throwing", exception.Message); - var channel = Channel.CreateUnbounded(); - resolver.OnEndPointsUpdated = result => channel.Writer.TryWrite(result); + var channel = Channel.CreateUnbounded(); + watcher.OnEndpointsUpdated = result => channel.Writer.TryWrite(result); do { cts[0].Cancel(); sem.Release(1); - var resolveTask = resolver.GetEndPointsAsync(CancellationToken.None); + var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); await resolveTask.ConfigureAwait(false); var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); if (next.ResolvedSuccessfully) @@ -277,11 +277,11 @@ public async Task ResolveServiceEndPoint_ThrowOnReload() } } while (true); - var task = resolver.GetEndPointsAsync(CancellationToken.None); + var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var result = await task.ConfigureAwait(false); - Assert.NotSame(initialEndPoints, result); - var sep = Assert.Single(result.EndPoints); + 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); From 27dd319ccfa2560e14d9092100a45c510a6e22eb Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 12 Apr 2024 13:35:12 +1000 Subject: [PATCH 41/77] Enable public API analyzer. (#3547) * Enable public API analyzer. --- .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 28 ++++++++++++++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 32 +++++++++++++++++++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 5 +++ .../PublicAPI.Shipped.txt | 1 + .../PublicAPI.Unshipped.txt | 30 +++++++++++++++++ 8 files changed, 99 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..b55e2b696ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt @@ -0,0 +1,28 @@ +#nullable enable +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..aa1fee77235 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt @@ -0,0 +1,32 @@ +#nullable enable +Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..55d92fd4caa --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt @@ -0,0 +1,5 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..7dc5c58110b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..b3be4048a2d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt @@ -0,0 +1,30 @@ +#nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From 6b67ac719c62ae5ba1a684a6b59614fd7665da1b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 19 Apr 2024 13:54:03 -0700 Subject: [PATCH 42/77] Service Discovery add additional configuration tests, make scheme selection more intuitive in un-specified case (#3837) --- .../ConfigurationServiceEndpointProvider.cs | 88 +++++---- .../ServiceDiscoveryOptions.cs | 34 ++-- ...nfigurationServiceEndpointResolverTests.cs | 178 ++++++++++++++++-- 3 files changed, 225 insertions(+), 75 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 37078e01969..e8c84b69ec8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -17,6 +17,7 @@ internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEnd private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly bool _includeAllSchemes; private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -39,6 +40,7 @@ public ConfigurationServiceEndpointProvider( { _serviceName = query.ServiceName; _endpointName = query.EndpointName; + _includeAllSchemes = serviceDiscoveryOptions.Value.AllowAllSchemes && query.IncludedSchemes.Count == 0; _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; @@ -74,27 +76,17 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo string endpointName; if (string.IsNullOrWhiteSpace(_endpointName)) { - if (_schemes.Length == 0) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + endpointName = DefaultEndpointName; + ReadOnlySpan candidateNames = [DefaultEndpointName, .. _schemes]; + foreach (var scheme in candidateNames) { - // Use the section named "default". - endpointName = DefaultEndpointName; - namedSection = section.GetSection(endpointName); - } - else - { - // Set the ideal endpoint name for error messages. - endpointName = _schemes[0]; - - // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists - foreach (var scheme in _schemes) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - var candidate = section.GetSection(scheme); - if (candidate.Exists()) - { - endpointName = scheme; - namedSection = candidate; - break; - } + endpointName = scheme; + namedSection = candidate; + break; } } } @@ -135,46 +127,60 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo } } - // Filter the resolved endpoints to only include those which match the specified scheme. - var minIndex = _schemes.Length; - foreach (var ep in resolved) + int resolvedEndpointCount; + if (_includeAllSchemes) { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Include all endpoints. + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index < minIndex) - { - minIndex = index; - } + endpoints.Endpoints.Add(ep); } + + resolvedEndpointCount = resolved.Count; } - - var added = 0; - foreach (var ep in resolved) + else { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Filter the resolved endpoints to only include those which match the specified, allowed schemes. + resolvedEndpointCount = 0; + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index <= minIndex) + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) { - ++added; - endpoints.Endpoints.Add(ep); + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } } } - else + + foreach (var ep in resolved) { - ++added; - endpoints.Endpoints.Add(ep); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } + } + else + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } } } - if (added == 0) + if (resolvedEndpointCount == 0) { Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); } else { - Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, resolvedEndpointCount); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 02ce1af162b..edc652507d9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -32,29 +32,35 @@ public sealed class ServiceDiscoveryOptions internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowAllSchemes) + if (schemes.Count > 0) { - if (schemes is string[] array) + if (allowAllSchemes) { - return array; - } + if (schemes is string[] array && array.Length > 0) + { + return array; + } - return schemes.ToArray(); - } + return schemes.ToArray(); + } - List result = []; - foreach (var s in schemes) - { - foreach (var allowed in allowedSchemes) + List result = []; + foreach (var s in schemes) { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + foreach (var allowed in allowedSchemes) { - result.Add(s); - break; + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } } } + + return result.ToArray(); } - return result.ToArray(); + // If no schemes were specified, but a set of allowed schemes were specified, allow those. + return allowedSchemes.ToArray(); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index db720782107..6955cc1e8e2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndpoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -87,8 +87,23 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() Assert.Empty(initialResult.EndpointSource.Endpoints); } + // Specifying no scheme. + // We should get the HTTPS endpoint back, since it is explicitly allowed + await using ((watcher = watcherFactory.CreateWatcher("_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + // Specifying either https or http. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -103,7 +118,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } // Specifying either https or http, but in reverse. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -118,6 +133,144 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } } + [Fact] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:8080", + ["services:basket:otlp:0"] = "https://localhost:8888", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider(o => + { + o.ShouldApplyHostNameMetadata = _ => true; + }) + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Explicitly specifying https as the scheme, but the endpoint section in configuration is the default value ("default"). + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("https://basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + + Assert.All(initialResult.EndpointSource.Endpoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Not specifying the scheme or endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + + // Not specifying the scheme, but specifying the default endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("_default.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + } + + /// + /// Checks that when there is no named endpoint, configuration resolves first from the "default" section, then sections named by the scheme names. + /// + [Theory] + [InlineData(true, true, "https://basket", "https://default-host:8080")] + [InlineData(false, true, "https://basket","https://https-host:8080")] + [InlineData(true, false, "https://basket", "https://default-host:8080")] + [InlineData(true, true, "basket", "https://default-host:8080")] + [InlineData(false, true, "basket", null)] + [InlineData(true, false, "basket", "https://default-host:8080")] + [InlineData(true, true, "http+https://basket", "https://default-host:8080")] + [InlineData(false, true, "http+https://basket","https://https-host:8080")] + [InlineData(true, false, "http+https://basket", "https://default-host:8080")] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_ResolutionOrder( + bool includeDefault, + bool includeSchemeNamed, + string serviceName, + string? expectedResult) + { + var data = new Dictionary(); + if (includeDefault) + { + data["services:basket:default:0"] = "https://default-host:8080"; + } + + if (includeSchemeNamed) + { + data["services:basket:https:0"] = "https://https-host:8080"; + } + + var config = new ConfigurationBuilder().AddInMemoryCollection(data); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Scheme in query + await using ((watcher = watcherFactory.CreateWatcher(serviceName)).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + if (expectedResult is not null) + { + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + else + { + Assert.Empty(initialResult.EndpointSource.Endpoints); + } + } + } + [Fact] public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { @@ -125,8 +278,8 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:http:0"] = "http://localhost:8080", - ["services:basket:http:1"] = "http://remotehost:9090", + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -274,19 +427,4 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe }); } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } From 8ccf3386b25bbf8c7c960d913b5f0412d15ae661 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Apr 2024 13:58:54 -0700 Subject: [PATCH 43/77] Service Discovery add additional configuration tests, make scheme selection more intuitive in un-specified case (#3848) Co-authored-by: Reuben Bond --- .../ConfigurationServiceEndpointProvider.cs | 88 +++++---- .../ServiceDiscoveryOptions.cs | 34 ++-- ...nfigurationServiceEndpointResolverTests.cs | 178 ++++++++++++++++-- 3 files changed, 225 insertions(+), 75 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs index 37078e01969..e8c84b69ec8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs @@ -17,6 +17,7 @@ internal sealed partial class ConfigurationServiceEndpointProvider : IServiceEnd private const string DefaultEndpointName = "default"; private readonly string _serviceName; private readonly string? _endpointName; + private readonly bool _includeAllSchemes; private readonly string[] _schemes; private readonly IConfiguration _configuration; private readonly ILogger _logger; @@ -39,6 +40,7 @@ public ConfigurationServiceEndpointProvider( { _serviceName = query.ServiceName; _endpointName = query.EndpointName; + _includeAllSchemes = serviceDiscoveryOptions.Value.AllowAllSchemes && query.IncludedSchemes.Count == 0; _schemes = ServiceDiscoveryOptions.ApplyAllowedSchemes(query.IncludedSchemes, serviceDiscoveryOptions.Value.AllowedSchemes, serviceDiscoveryOptions.Value.AllowAllSchemes); _configuration = configuration; _logger = logger; @@ -74,27 +76,17 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo string endpointName; if (string.IsNullOrWhiteSpace(_endpointName)) { - if (_schemes.Length == 0) + // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists + endpointName = DefaultEndpointName; + ReadOnlySpan candidateNames = [DefaultEndpointName, .. _schemes]; + foreach (var scheme in candidateNames) { - // Use the section named "default". - endpointName = DefaultEndpointName; - namedSection = section.GetSection(endpointName); - } - else - { - // Set the ideal endpoint name for error messages. - endpointName = _schemes[0]; - - // Treat the scheme as the endpoint name and use the first section with a matching endpoint name which exists - foreach (var scheme in _schemes) + var candidate = section.GetSection(scheme); + if (candidate.Exists()) { - var candidate = section.GetSection(scheme); - if (candidate.Exists()) - { - endpointName = scheme; - namedSection = candidate; - break; - } + endpointName = scheme; + namedSection = candidate; + break; } } } @@ -135,46 +127,60 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo } } - // Filter the resolved endpoints to only include those which match the specified scheme. - var minIndex = _schemes.Length; - foreach (var ep in resolved) + int resolvedEndpointCount; + if (_includeAllSchemes) { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Include all endpoints. + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index < minIndex) - { - minIndex = index; - } + endpoints.Endpoints.Add(ep); } + + resolvedEndpointCount = resolved.Count; } - - var added = 0; - foreach (var ep in resolved) + else { - if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + // Filter the resolved endpoints to only include those which match the specified, allowed schemes. + resolvedEndpointCount = 0; + var minIndex = _schemes.Length; + foreach (var ep in resolved) { - var index = Array.IndexOf(_schemes, scheme); - if (index >= 0 && index <= minIndex) + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) { - ++added; - endpoints.Endpoints.Add(ep); + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index < minIndex) + { + minIndex = index; + } } } - else + + foreach (var ep in resolved) { - ++added; - endpoints.Endpoints.Add(ep); + if (ep.EndPoint is UriEndPoint uri && uri.Uri.Scheme is { } scheme) + { + var index = Array.IndexOf(_schemes, scheme); + if (index >= 0 && index <= minIndex) + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } + } + else + { + ++resolvedEndpointCount; + endpoints.Endpoints.Add(ep); + } } } - if (added == 0) + if (resolvedEndpointCount == 0) { Log.ServiceConfigurationNotFound(_logger, _serviceName, configPath); } else { - Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, added); + Log.ConfiguredEndpoints(_logger, _serviceName, configPath, endpoints.Endpoints, resolvedEndpointCount); } return default; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs index 02ce1af162b..edc652507d9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs @@ -32,29 +32,35 @@ public sealed class ServiceDiscoveryOptions internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes) { - if (allowAllSchemes) + if (schemes.Count > 0) { - if (schemes is string[] array) + if (allowAllSchemes) { - return array; - } + if (schemes is string[] array && array.Length > 0) + { + return array; + } - return schemes.ToArray(); - } + return schemes.ToArray(); + } - List result = []; - foreach (var s in schemes) - { - foreach (var allowed in allowedSchemes) + List result = []; + foreach (var s in schemes) { - if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + foreach (var allowed in allowedSchemes) { - result.Add(s); - break; + if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase)) + { + result.Add(s); + break; + } } } + + return result.ToArray(); } - return result.ToArray(); + // If no schemes were specified, but a set of allowed schemes were specified, allow those. + return allowedSchemes.ToArray(); } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index db720782107..6955cc1e8e2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -18,7 +18,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Tests; public class ConfigurationServiceEndpointResolverTests { [Fact] - public async Task ResolveServiceEndpoint_Configuration_SingleResult() + public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() { var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { @@ -87,8 +87,23 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() Assert.Empty(initialResult.EndpointSource.Endpoints); } + // Specifying no scheme. + // We should get the HTTPS endpoint back, since it is explicitly allowed + await using ((watcher = watcherFactory.CreateWatcher("_foo.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + var ep = Assert.Single(initialResult.EndpointSource.Endpoints); + Assert.Equal(new UriEndPoint(new Uri("https://localhost")), ep.EndPoint); + } + // Specifying either https or http. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("https+http://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -103,7 +118,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } // Specifying either https or http, but in reverse. - // The result should be that we only get the http endpoint back. + // We should only get the https endpoint back. await using ((watcher = watcherFactory.CreateWatcher("http+https://_foo.basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); @@ -118,6 +133,144 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() } } + [Fact] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:8080", + ["services:basket:otlp:0"] = "https://localhost:8888", + }); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider(o => + { + o.ShouldApplyHostNameMetadata = _ => true; + }) + .Configure(o => + { + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Explicitly specifying https as the scheme, but the endpoint section in configuration is the default value ("default"). + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("https://basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + + Assert.All(initialResult.EndpointSource.Endpoints, ep => + { + var hostNameFeature = ep.Features.Get(); + Assert.NotNull(hostNameFeature); + Assert.Equal("basket", hostNameFeature.HostName); + }); + } + + // Not specifying the scheme or endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + + // Not specifying the scheme, but specifying the default endpoint name. + // We should get the endpoint back because it is an https endpoint (allowed) with the default endpoint name. + await using ((watcher = watcherFactory.CreateWatcher("_default.basket")).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + } + + /// + /// Checks that when there is no named endpoint, configuration resolves first from the "default" section, then sections named by the scheme names. + /// + [Theory] + [InlineData(true, true, "https://basket", "https://default-host:8080")] + [InlineData(false, true, "https://basket","https://https-host:8080")] + [InlineData(true, false, "https://basket", "https://default-host:8080")] + [InlineData(true, true, "basket", "https://default-host:8080")] + [InlineData(false, true, "basket", null)] + [InlineData(true, false, "basket", "https://default-host:8080")] + [InlineData(true, true, "http+https://basket", "https://default-host:8080")] + [InlineData(false, true, "http+https://basket","https://https-host:8080")] + [InlineData(true, false, "http+https://basket", "https://default-host:8080")] + public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_ResolutionOrder( + bool includeDefault, + bool includeSchemeNamed, + string serviceName, + string? expectedResult) + { + var data = new Dictionary(); + if (includeDefault) + { + data["services:basket:default:0"] = "https://default-host:8080"; + } + + if (includeSchemeNamed) + { + data["services:basket:https:0"] = "https://https-host:8080"; + } + + var config = new ConfigurationBuilder().AddInMemoryCollection(data); + var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var watcherFactory = services.GetRequiredService(); + ServiceEndpointWatcher watcher; + + // Scheme in query + await using ((watcher = watcherFactory.CreateWatcher(serviceName)).ConfigureAwait(false)) + { + Assert.NotNull(watcher); + var tcs = new TaskCompletionSource(); + watcher.OnEndpointsUpdated = tcs.SetResult; + watcher.Start(); + var initialResult = await tcs.Task.ConfigureAwait(false); + Assert.NotNull(initialResult); + Assert.True(initialResult.ResolvedSuccessfully); + if (expectedResult is not null) + { + Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); + } + else + { + Assert.Empty(initialResult.EndpointSource.Endpoints); + } + } + } + [Fact] public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { @@ -125,8 +278,8 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() { InitialData = new Dictionary { - ["services:basket:http:0"] = "http://localhost:8080", - ["services:basket:http:1"] = "http://remotehost:9090", + ["services:basket:default:0"] = "http://localhost:8080", + ["services:basket:default:1"] = "http://remotehost:9090", } }; var config = new ConfigurationBuilder().Add(configSource); @@ -274,19 +427,4 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe }); } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } From 696c6ce5c6677eaa981852c7045a41a1e7cc1bbb Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 30 Apr 2024 07:00:03 -0700 Subject: [PATCH 44/77] ServiceEndpoint.ToString(): omit zero port (#4015) --- .../Internal/ServiceEndpointImpl.cs | 4 ++ .../ServiceEndpointTests.cs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 8bfb50fe930..6f89cd8e37b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -9,9 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); + public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", + DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", _ => EndPoint.ToString()! }; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs new file mode 100644 index 00000000000..e05d0818e1a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -0,0 +1,42 @@ +// 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; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +public class ServiceEndpointTests +{ + public static TheoryData ZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:0"), + new DnsEndPoint("microsoft.com", 0), + new UriEndPoint(new Uri("https://microsoft.com")) + }; + + public static TheoryData NonZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:8443"), + new DnsEndPoint("microsoft.com", 8443), + new UriEndPoint(new Uri("https://microsoft.com:8443")) + }; + + [Theory] + [MemberData(nameof(ZeroPortEndPoints))] + public void ServiceEndpointToStringOmitsUnspecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.DoesNotContain(":0", epString); + } + + [Theory] + [MemberData(nameof(NonZeroPortEndPoints))] + public void ServiceEndpointToStringContainsSpecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.Contains(":8443", epString); + } +} From 131f376a261f594d1a2c06c2901715c0ebb0970d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 11:49:07 -0700 Subject: [PATCH 45/77] ServiceEndpoint.ToString(): omit zero port (#4033) --- .../Internal/ServiceEndpointImpl.cs | 4 ++ .../ServiceEndpointTests.cs | 42 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 8bfb50fe930..6f89cd8e37b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -9,9 +9,13 @@ namespace Microsoft.Extensions.ServiceDiscovery.Internal; internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? features = null) : ServiceEndpoint { public override EndPoint EndPoint { get; } = endPoint; + public override IFeatureCollection Features { get; } = features ?? new FeatureCollection(); + public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", + DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", _ => EndPoint.ToString()! }; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs new file mode 100644 index 00000000000..e05d0818e1a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -0,0 +1,42 @@ +// 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; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +public class ServiceEndpointTests +{ + public static TheoryData ZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:0"), + new DnsEndPoint("microsoft.com", 0), + new UriEndPoint(new Uri("https://microsoft.com")) + }; + + public static TheoryData NonZeroPortEndPoints => new() + { + IPEndPoint.Parse("127.0.0.1:8443"), + new DnsEndPoint("microsoft.com", 8443), + new UriEndPoint(new Uri("https://microsoft.com:8443")) + }; + + [Theory] + [MemberData(nameof(ZeroPortEndPoints))] + public void ServiceEndpointToStringOmitsUnspecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.DoesNotContain(":0", epString); + } + + [Theory] + [MemberData(nameof(NonZeroPortEndPoints))] + public void ServiceEndpointToStringContainsSpecifiedPort(EndPoint endpoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endpoint); + var epString = serviceEndpoint.ToString(); + Assert.Contains(":8443", epString); + } +} From 7de9f20fb3b8180841d3a1bd40a62931dd00d04e Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:26:27 -0700 Subject: [PATCH 46/77] Promptly remove invalid resolvers (#3800) --- .../Http/HttpServiceEndpointResolver.cs | 5 ++ .../ServiceEndpointResolver.cs | 6 +++ .../ServiceEndpointWatcher.cs | 48 +++++++++++++------ .../DnsServiceEndpointResolverTests.cs | 35 ++++++++++++++ .../DnsSrvServiceEndpointResolverTests.cs | 19 +------- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 1 + 6 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index e11f593776f..e547ab14138 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -57,6 +57,10 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ return endpoint; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -140,6 +144,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 92df120940d..e928980700c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -49,6 +49,7 @@ public async ValueTask GetEndpointsAsync(string serviceNa while (true) { ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); var resolver = _resolvers.GetOrAdd( serviceName, static (name, self) => self.CreateResolver(name), @@ -64,6 +65,10 @@ public async ValueTask GetEndpointsAsync(string serviceNa return result; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -148,6 +153,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index ba6df9b43c4..d361202a698 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -32,6 +32,7 @@ internal sealed partial class ServiceEndpointWatcher( private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; + private IDisposable? _changeTokenRegistration; /// /// Gets the service name. @@ -60,6 +61,8 @@ public void Start() public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoProviders(); + ObjectDisposedException.ThrowIf(_disposalCancellation.IsCancellationRequested, this); + cancellationToken.ThrowIfCancellationRequested(); // If the cache is valid, return the cached value. if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) @@ -76,9 +79,11 @@ async ValueTask GetEndpointsInternal(CancellationToken ca ServiceEndpointSource? result; do { + cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; } while (result is null); + return result; } } @@ -89,7 +94,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) + if (!_disposalCancellation.IsCancellationRequested && _refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -128,10 +133,18 @@ private async Task RefreshAsyncInternal() CacheStatus newCacheState; try { + lock (_lock) + { + // Dispose the existing change token registration, if any. + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + } + Log.ResolvingEndpoints(_logger, ServiceName); var builder = new ServiceEndpointBuilder(); foreach (var provider in _providers) { + cancellationToken.ThrowIfCancellationRequested(); await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } @@ -143,13 +156,12 @@ private async Task RefreshAsyncInternal() // 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(); - } + // Initiate a background refresh when the change token fires. + _changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); + + // Dispose the existing timer, if any, since we are reliant on change tokens for updates. + _pollingTimer?.Dispose(); + _pollingTimer = null; } else { @@ -211,6 +223,13 @@ private void SchedulePollingTimer() { lock (_lock) { + if (_disposalCancellation.IsCancellationRequested) + { + _pollingTimer?.Dispose(); + _pollingTimer = null; + return; + } + if (_pollingTimer is null) { _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); @@ -227,14 +246,15 @@ public async ValueTask DisposeAsync() { lock (_lock) { - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + _disposalCancellation.Cancel(); + + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + + _pollingTimer?.Dispose(); + _pollingTimer = null; } - _disposalCancellation.Cancel(); if (_refreshTask is { } task) { await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs new file mode 100644 index 00000000000..2b3a7fd7cd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs @@ -0,0 +1,35 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServiceEndpointResolverTests +{ + [Fact] + public async Task ResolveServiceEndpoint_Dns_MultiShot() + { + var timeProvider = new FakeTimeProvider(); + var services = new ServiceCollection() + .AddSingleton(timeProvider) + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) + .BuildServiceProvider(); + var resolver = services.GetRequiredService(); + var initialResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(initialResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(7)); + var secondResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(secondResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(80)); + var thirdResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(thirdResult); + Assert.True(initialResult.Endpoints.Count > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 7cadb4e3c7f..3449c075047 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndpoint_Dns() + public async Task ResolveServiceEndpoint_DnsSrv() { var dnsClientMock = new FakeDnsClient { @@ -134,7 +134,7 @@ public async Task ResolveServiceEndpoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -233,19 +233,4 @@ public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(boo } } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index ba827640199..31fe0f9d687 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -9,6 +9,7 @@ + From b62eb8eabbf9e713c3b505a5d37f909ac7bce9b0 Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 1 May 2024 08:43:39 -0700 Subject: [PATCH 47/77] Improve test coverage for YARP Service Discovery (#4036) --- .../Internal/ServiceEndpointImpl.cs | 1 + ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 4 + .../ServiceDiscoveryDestinationResolver.cs | 32 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 23 ++ .../YarpServiceDiscoveryTests.cs | 284 ++++++++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 6f89cd8e37b..151a9309338 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -14,6 +14,7 @@ internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 => $"[{ip.Address}]", IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index c5145e06528..08f6394daf8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 8ec810f2c05..3de03a6ebde 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -1,6 +1,7 @@ // 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.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.ServiceDiscovery; @@ -14,8 +15,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver +/// The service discovery options. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver, IOptions options) : IDestinationResolver { + private readonly ServiceDiscoveryOptions _options = options.Value; + /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) { @@ -65,13 +69,15 @@ public async ValueTask ResolveDestinationsAsync(I Uri uri; if (!addressString.Contains("://")) { - uri = new Uri($"https://{addressString}"); + var scheme = GetDefaultScheme(originalUri); + uri = new Uri($"{scheme}://{addressString}"); } else { uri = new Uri(addressString); } + uriBuilder.Scheme = uri.Scheme; uriBuilder.Host = uri.Host; uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); @@ -90,4 +96,26 @@ public async ValueTask ResolveDestinationsAsync(I return (results, result.ChangeToken); } + + private string GetDefaultScheme(Uri originalUri) + { + if (originalUri.Scheme.IndexOf('+') > 0) + { + // Use the first allowed scheme. + var specifiedSchemes = originalUri.Scheme.Split('+'); + foreach (var scheme in specifiedSchemes) + { + if (_options.AllowAllSchemes || _options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + return scheme; + } + } + + throw new InvalidOperationException($"None of the specified schemes ('{string.Join(", ", specifiedSchemes)}') are allowed by configuration."); + } + else + { + return originalUri.Scheme; + } + } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj new file mode 100644 index 00000000000..8a816222c9f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs new file mode 100644 index 00000000000..3628da0839f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -0,0 +1,284 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Yarp.ReverseProxy.Configuration; +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +/// +/// Tests for YARP with Service Discovery enabled. +/// +public class YarpServiceDiscoveryTests +{ + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://my-svc/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "https://localhost:8888", + ["services:basket:default:2"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://localhost:8888/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPreferredScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("http://localhost:1111/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .Configure(o => + { + // Allow only "https://" + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + // No results: there are no 'https' endpoints in config and 'http' is disallowed. + Assert.Equal(0, result.Destinations.Count); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Dns() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://microsoft.com", + }, + ["dest-b"] = new() + { + Address = "http://msn.com", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + Assert.NotNull(result); + Assert.NotEmpty(result.Destinations); + Assert.All(result.Destinations, d => + { + var address = d.Value.Address; + Assert.True(Uri.TryCreate(address, default, out var uri), $"Failed to parse address '{address}' as URI."); + Assert.True(uri.IsDefaultPort, "URI should use the default port when resolved via DNS."); + var expectedScheme = d.Key.StartsWith("dest-a") ? "https" : "http"; + Assert.Equal(expectedScheme, uri.Scheme); + }); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), + } + }; + + return Task.FromResult(response); + } + }; + + await using var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(3, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://10.10.10.10:8888/", a), + a => Assert.Equal("https://[::1]:9999/", a), + a => Assert.Equal("https://127.0.0.1:7777/", a)); + } + + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } +} From 40992dab676f8aa7b2c55e46226e882f1a02d9ff Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Wed, 1 May 2024 13:14:14 -0700 Subject: [PATCH 48/77] Move Cancel call outside of lock and add additional error handling (#4052) --- .../ServiceEndpointWatcher.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index d361202a698..a94b7b7a3c1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -77,8 +77,10 @@ public ValueTask GetEndpointsAsync(CancellationToken canc async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { ServiceEndpointSource? result; + var disposalToken = _disposalCancellation.Token; do { + disposalToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; @@ -194,7 +196,14 @@ private async Task RefreshAsyncInternal() if (OnEndpointsUpdated is { } callback) { - callback(new(newEndpoints, error)); + try + { + callback(new(newEndpoints, error)); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error notifying observers of updated endpoints."); + } } lock (_lock) @@ -244,10 +253,17 @@ private void SchedulePollingTimer() /// public async ValueTask DisposeAsync() { - lock (_lock) + try { _disposalCancellation.Cancel(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error cancelling disposal cancellation token."); + } + lock (_lock) + { _changeTokenRegistration?.Dispose(); _changeTokenRegistration = null; From dcd995db36b30341157645b0fdee2134832045f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 14:24:23 -0600 Subject: [PATCH 49/77] Improve test coverage for YARP Service Discovery (#4051) Co-authored-by: Reuben Bond --- .../Internal/ServiceEndpointImpl.cs | 1 + ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 4 + .../ServiceDiscoveryDestinationResolver.cs | 32 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 23 ++ .../YarpServiceDiscoveryTests.cs | 284 ++++++++++++++++++ 5 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs index 6f89cd8e37b..151a9309338 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Internal/ServiceEndpointImpl.cs @@ -14,6 +14,7 @@ internal sealed class ServiceEndpointImpl(EndPoint endPoint, IFeatureCollection? public override string? ToString() => EndPoint switch { + IPEndPoint ip when ip.Port == 0 && ip.AddressFamily == System.Net.Sockets.AddressFamily.InterNetworkV6 => $"[{ip.Address}]", IPEndPoint ip when ip.Port == 0 => $"{ip.Address}", DnsEndPoint dns when dns.Port == 0 => $"{dns.Host}", DnsEndPoint dns => $"{dns.Host}:{dns.Port}", diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index c5145e06528..08f6394daf8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 8ec810f2c05..3de03a6ebde 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -1,6 +1,7 @@ // 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.Primitives; using Yarp.ReverseProxy.Configuration; using Yarp.ReverseProxy.ServiceDiscovery; @@ -14,8 +15,11 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp; /// Initializes a new instance. /// /// The endpoint resolver registry. -internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver) : IDestinationResolver +/// The service discovery options. +internal sealed class ServiceDiscoveryDestinationResolver(ServiceEndpointResolver resolver, IOptions options) : IDestinationResolver { + private readonly ServiceDiscoveryOptions _options = options.Value; + /// public async ValueTask ResolveDestinationsAsync(IReadOnlyDictionary destinations, CancellationToken cancellationToken) { @@ -65,13 +69,15 @@ public async ValueTask ResolveDestinationsAsync(I Uri uri; if (!addressString.Contains("://")) { - uri = new Uri($"https://{addressString}"); + var scheme = GetDefaultScheme(originalUri); + uri = new Uri($"{scheme}://{addressString}"); } else { uri = new Uri(addressString); } + uriBuilder.Scheme = uri.Scheme; uriBuilder.Host = uri.Host; uriBuilder.Port = uri.Port; var resolvedAddress = uriBuilder.Uri.ToString(); @@ -90,4 +96,26 @@ public async ValueTask ResolveDestinationsAsync(I return (results, result.ChangeToken); } + + private string GetDefaultScheme(Uri originalUri) + { + if (originalUri.Scheme.IndexOf('+') > 0) + { + // Use the first allowed scheme. + var specifiedSchemes = originalUri.Scheme.Split('+'); + foreach (var scheme in specifiedSchemes) + { + if (_options.AllowAllSchemes || _options.AllowedSchemes.Contains(scheme, StringComparer.OrdinalIgnoreCase)) + { + return scheme; + } + } + + throw new InvalidOperationException($"None of the specified schemes ('{string.Join(", ", specifiedSchemes)}') are allowed by configuration."); + } + else + { + return originalUri.Scheme; + } + } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj new file mode 100644 index 00000000000..8a816222c9f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -0,0 +1,23 @@ + + + + $(NetCurrent) + enable + enable + + + + + + + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs new file mode 100644 index 00000000000..3628da0839f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -0,0 +1,284 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; +using Yarp.ReverseProxy.Configuration; +using System.Net; +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +/// +/// Tests for YARP with Service Discovery enabled. +/// +public class YarpServiceDiscoveryTests +{ + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddPassThroughServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://my-svc/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "https://localhost:8888", + ["services:basket:default:2"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://localhost:8888/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPreferredScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(1, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("http://localhost:1111/", a)); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "ftp://localhost:2121", + ["services:basket:default:1"] = "http://localhost:1111", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .Configure(o => + { + // Allow only "https://" + o.AllowAllSchemes = false; + o.AllowedSchemes = ["https"]; + }) + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https+http://basket", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + // No results: there are no 'https' endpoints in config and 'http' is disallowed. + Assert.Equal(0, result.Destinations.Count); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_Dns() + { + await using var services = new ServiceCollection() + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider() + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://microsoft.com", + }, + ["dest-b"] = new() + { + Address = "http://msn.com", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + Assert.NotNull(result); + Assert.NotEmpty(result.Destinations); + Assert.All(result.Destinations, d => + { + var address = d.Value.Address; + Assert.True(Uri.TryCreate(address, default, out var uri), $"Failed to parse address '{address}' as URI."); + Assert.True(uri.IsDefaultPort, "URI should use the default port when resolved via DNS."); + var expectedScheme = d.Key.StartsWith("dest-a") ? "https" : "http"; + Assert.Equal(expectedScheme, uri.Scheme); + }); + } + + [Fact] + public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() + { + var dnsClientMock = new FakeDnsClient + { + QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + { + var response = new FakeDnsQueryResponse + { + Answers = new List + { + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), + new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) + }, + Additionals = new List + { + new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), + new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), + new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), + } + }; + + return Task.FromResult(response); + } + }; + + await using var services = new ServiceCollection() + .AddSingleton(dnsClientMock) + .AddServiceDiscoveryCore() + .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") + .BuildServiceProvider(); + var coreResolver = services.GetRequiredService(); + var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://my-svc", + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(3, result.Destinations.Count); + Assert.Collection(result.Destinations.Select(d => d.Value.Address), + a => Assert.Equal("https://10.10.10.10:8888/", a), + a => Assert.Equal("https://[::1]:9999/", a), + a => Assert.Equal("https://127.0.0.1:7777/", a)); + } + + private sealed class FakeDnsClient : IDnsQuery + { + public Func>? QueryAsyncFunc { get; set; } + + public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) + => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); + public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); + public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + } + + private sealed class FakeDnsQueryResponse : IDnsQueryResponse + { + public IReadOnlyList? Questions { get; set; } + public IReadOnlyList? Additionals { get; set; } + public IEnumerable? AllRecords { get; set; } + public IReadOnlyList? Answers { get; set; } + public IReadOnlyList? Authorities { get; set; } + public string? AuditTrail { get; set; } + public string? ErrorMessage { get; set; } + public bool HasError { get; set; } + public DnsResponseHeader? Header { get; set; } + public int MessageSize { get; set; } + public NameServer? NameServer { get; set; } + public DnsQuerySettings? Settings { get; set; } + } +} From 3f610e1ba52b7ff7a44bce08443dc980e874b43b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 15:22:38 -0700 Subject: [PATCH 50/77] Promptly remove invalid resolvers (#4039) Co-authored-by: ReubenBond --- .../Http/HttpServiceEndpointResolver.cs | 5 ++ .../ServiceEndpointResolver.cs | 6 +++ .../ServiceEndpointWatcher.cs | 48 +++++++++++++------ .../DnsServiceEndpointResolverTests.cs | 35 ++++++++++++++ .../DnsSrvServiceEndpointResolverTests.cs | 19 +------- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 1 + 6 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs index e11f593776f..e547ab14138 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs @@ -57,6 +57,10 @@ public async ValueTask GetEndpointAsync(HttpRequestMessage requ return endpoint; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -140,6 +144,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs index 92df120940d..e928980700c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs @@ -49,6 +49,7 @@ public async ValueTask GetEndpointsAsync(string serviceNa while (true) { ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); var resolver = _resolvers.GetOrAdd( serviceName, static (name, self) => self.CreateResolver(name), @@ -64,6 +65,10 @@ public async ValueTask GetEndpointsAsync(string serviceNa return result; } + else + { + _resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver)); + } } } @@ -148,6 +153,7 @@ private async Task CleanupResolversAsyncCore() cleanupTasks.Add(resolver.DisposeAsync().AsTask()); } } + if (cleanupTasks is not null) { await Task.WhenAll(cleanupTasks).ConfigureAwait(false); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index ba6df9b43c4..d361202a698 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -32,6 +32,7 @@ internal sealed partial class ServiceEndpointWatcher( private ServiceEndpointSource? _cachedEndpoints; private Task _refreshTask = Task.CompletedTask; private volatile CacheStatus _cacheState; + private IDisposable? _changeTokenRegistration; /// /// Gets the service name. @@ -60,6 +61,8 @@ public void Start() public ValueTask GetEndpointsAsync(CancellationToken cancellationToken = default) { ThrowIfNoProviders(); + ObjectDisposedException.ThrowIf(_disposalCancellation.IsCancellationRequested, this); + cancellationToken.ThrowIfCancellationRequested(); // If the cache is valid, return the cached value. if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached) @@ -76,9 +79,11 @@ async ValueTask GetEndpointsInternal(CancellationToken ca ServiceEndpointSource? result; do { + cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; } while (result is null); + return result; } } @@ -89,7 +94,7 @@ private Task RefreshAsync(bool force) lock (_lock) { // If the cache is invalid or needs invalidation, refresh the cache. - if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) + if (!_disposalCancellation.IsCancellationRequested && _refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force)) { // Indicate that the cache is being updated and start a new refresh task. _cacheState = CacheStatus.Refreshing; @@ -128,10 +133,18 @@ private async Task RefreshAsyncInternal() CacheStatus newCacheState; try { + lock (_lock) + { + // Dispose the existing change token registration, if any. + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + } + Log.ResolvingEndpoints(_logger, ServiceName); var builder = new ServiceEndpointBuilder(); foreach (var provider in _providers) { + cancellationToken.ThrowIfCancellationRequested(); await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false); } @@ -143,13 +156,12 @@ private async Task RefreshAsyncInternal() // 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(); - } + // Initiate a background refresh when the change token fires. + _changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this); + + // Dispose the existing timer, if any, since we are reliant on change tokens for updates. + _pollingTimer?.Dispose(); + _pollingTimer = null; } else { @@ -211,6 +223,13 @@ private void SchedulePollingTimer() { lock (_lock) { + if (_disposalCancellation.IsCancellationRequested) + { + _pollingTimer?.Dispose(); + _pollingTimer = null; + return; + } + if (_pollingTimer is null) { _pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero); @@ -227,14 +246,15 @@ public async ValueTask DisposeAsync() { lock (_lock) { - if (_pollingTimer is { } timer) - { - _pollingTimer = null; - timer.Dispose(); - } + _disposalCancellation.Cancel(); + + _changeTokenRegistration?.Dispose(); + _changeTokenRegistration = null; + + _pollingTimer?.Dispose(); + _pollingTimer = null; } - _disposalCancellation.Cancel(); if (_refreshTask is { } task) { await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs new file mode 100644 index 00000000000..2b3a7fd7cd3 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServiceEndpointResolverTests.cs @@ -0,0 +1,35 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Time.Testing; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServiceEndpointResolverTests +{ + [Fact] + public async Task ResolveServiceEndpoint_Dns_MultiShot() + { + var timeProvider = new FakeTimeProvider(); + var services = new ServiceCollection() + .AddSingleton(timeProvider) + .AddServiceDiscoveryCore() + .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) + .BuildServiceProvider(); + var resolver = services.GetRequiredService(); + var initialResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(initialResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(7)); + var secondResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(secondResult); + Assert.True(initialResult.Endpoints.Count > 0); + timeProvider.Advance(TimeSpan.FromSeconds(80)); + var thirdResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None); + Assert.NotNull(thirdResult); + Assert.True(initialResult.Endpoints.Count > 0); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 7cadb4e3c7f..3449c075047 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse } [Fact] - public async Task ResolveServiceEndpoint_Dns() + public async Task ResolveServiceEndpoint_DnsSrv() { var dnsClientMock = new FakeDnsClient { @@ -134,7 +134,7 @@ public async Task ResolveServiceEndpoint_Dns() [InlineData(true)] [InlineData(false)] [Theory] - public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst) + public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { var dnsClientMock = new FakeDnsClient { @@ -233,19 +233,4 @@ public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(boo } } } - - public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource - { - public IConfigurationProvider Build(IConfigurationBuilder builder) => this; - public void SetValues(IEnumerable> values) - { - Data.Clear(); - foreach (var (key, value) in values) - { - Data[key] = value; - } - - OnReload(); - } - } } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index ba827640199..31fe0f9d687 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -9,6 +9,7 @@ + From 5e625228a817ee0cbb8ebd24a1b69666116ad6ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 23:38:28 +0000 Subject: [PATCH 51/77] Move Cancel call outside of lock and add additional error handling (#4055) Co-authored-by: Reuben Bond --- .../ServiceEndpointWatcher.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs index d361202a698..a94b7b7a3c1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs @@ -77,8 +77,10 @@ public ValueTask GetEndpointsAsync(CancellationToken canc async ValueTask GetEndpointsInternal(CancellationToken cancellationToken) { ServiceEndpointSource? result; + var disposalToken = _disposalCancellation.Token; do { + disposalToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested(); await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false); result = _cachedEndpoints; @@ -194,7 +196,14 @@ private async Task RefreshAsyncInternal() if (OnEndpointsUpdated is { } callback) { - callback(new(newEndpoints, error)); + try + { + callback(new(newEndpoints, error)); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error notifying observers of updated endpoints."); + } } lock (_lock) @@ -244,10 +253,17 @@ private void SchedulePollingTimer() /// public async ValueTask DisposeAsync() { - lock (_lock) + try { _disposalCancellation.Cancel(); + } + catch (Exception exception) + { + _logger.LogError(exception, "Error cancelling disposal cancellation token."); + } + lock (_lock) + { _changeTokenRegistration?.Dispose(); _changeTokenRegistration = null; From 1441356999a97e54572501024eb60b11259ffb9b Mon Sep 17 00:00:00 2001 From: Reuben Bond <203839+ReubenBond@users.noreply.github.com> Date: Fri, 3 May 2024 09:43:48 -0700 Subject: [PATCH 52/77] YARP: add special-case for localhost when setting Host value (#4069) --- .../ServiceDiscoveryDestinationResolver.cs | 24 +++- .../YarpServiceDiscoveryTests.cs | 109 ++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 3de03a6ebde..113e243565e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -55,7 +55,6 @@ public async ValueTask ResolveDestinationsAsync(I CancellationToken cancellationToken) { var originalUri = new Uri(originalConfig.Address); - var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); @@ -90,7 +89,28 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + string? resolvedHost; + + // Use the configured 'Host' value if it is provided. + if (!string.IsNullOrEmpty(originalConfig.Host)) + { + resolvedHost = originalConfig.Host; + } + else if (uri.IsLoopback) + { + // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. + // This is to account for non-wildcard development certificate. + resolvedHost = null; + } + else + { + // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] + // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host + // i.e, use Authority and not Host. + resolvedHost = originalUri.Authority; + } + + var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 3628da0839f..f2264e46411 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -18,6 +18,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; /// public class YarpServiceDiscoveryTests { + private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider) + { + var coreResolver = serviceProvider.GetRequiredService(); + return new ServiceDiscoveryDestinationResolver( + coreResolver, + serviceProvider.GetRequiredService>()); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() { @@ -25,8 +33,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -106,6 +111,89 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref a => Assert.Equal("http://localhost:1111/", a)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost) + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:1111", + ["services:basket:default:1"] = "https://127.0.0.1:2222", + ["services:basket:default:2"] = "https://[::1]:3333", + ["services:basket:default:3"] = "https://baskets-galore.faketld", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var yarpResolver = CreateResolver(services); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://basket", + Host = configHasHost ? "my-basket-svc.faketld" : null + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(4, result.Destinations.Count); + Assert.Collection(result.Destinations.Values, + a => + { + Assert.Equal("https://localhost:1111/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://127.0.0.1:2222/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://[::1]:3333/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://baskets-galore.faketld/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + // For non-localhost values, fallback to the input address. + Assert.Equal("basket", a.Host); + } + }); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() { @@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo }) .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { From 2855a7d98b6d4a4abd4265fb8b2efdf5b959c848 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 May 2024 17:26:05 -0700 Subject: [PATCH 53/77] YARP: add special-case for localhost when setting Host value (#4076) Co-authored-by: Reuben Bond --- .../ServiceDiscoveryDestinationResolver.cs | 24 +++- .../YarpServiceDiscoveryTests.cs | 109 ++++++++++++++++-- 2 files changed, 119 insertions(+), 14 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 3de03a6ebde..113e243565e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -55,7 +55,6 @@ public async ValueTask ResolveDestinationsAsync(I CancellationToken cancellationToken) { var originalUri = new Uri(originalConfig.Address); - var originalHost = originalConfig.Host is { Length: > 0 } h ? h : originalUri.Authority; var serviceName = originalUri.GetLeftPart(UriPartial.Authority); var result = await resolver.GetEndpointsAsync(serviceName, cancellationToken).ConfigureAwait(false); @@ -90,7 +89,28 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - var config = originalConfig with { Host = originalHost, Address = resolvedAddress, Health = healthAddress }; + string? resolvedHost; + + // Use the configured 'Host' value if it is provided. + if (!string.IsNullOrEmpty(originalConfig.Host)) + { + resolvedHost = originalConfig.Host; + } + else if (uri.IsLoopback) + { + // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. + // This is to account for non-wildcard development certificate. + resolvedHost = null; + } + else + { + // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] + // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host + // i.e, use Authority and not Host. + resolvedHost = originalUri.Authority; + } + + var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 3628da0839f..f2264e46411 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -18,6 +18,14 @@ namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; /// public class YarpServiceDiscoveryTests { + private static ServiceDiscoveryDestinationResolver CreateResolver(IServiceProvider serviceProvider) + { + var coreResolver = serviceProvider.GetRequiredService(); + return new ServiceDiscoveryDestinationResolver( + coreResolver, + serviceProvider.GetRequiredService>()); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() { @@ -25,8 +33,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() .AddServiceDiscoveryCore() .AddPassThroughServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -57,8 +64,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -88,8 +94,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref .AddServiceDiscoveryCore() .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -106,6 +111,89 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref a => Assert.Equal("http://localhost:1111/", a)); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Value(bool configHasHost) + { + var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary + { + ["services:basket:default:0"] = "https://localhost:1111", + ["services:basket:default:1"] = "https://127.0.0.1:2222", + ["services:basket:default:2"] = "https://[::1]:3333", + ["services:basket:default:3"] = "https://baskets-galore.faketld", + }); + await using var services = new ServiceCollection() + .AddSingleton(config.Build()) + .AddServiceDiscoveryCore() + .AddConfigurationServiceEndpointProvider() + .BuildServiceProvider(); + var yarpResolver = CreateResolver(services); + + var destinationConfigs = new Dictionary + { + ["dest-a"] = new() + { + Address = "https://basket", + Host = configHasHost ? "my-basket-svc.faketld" : null + }, + }; + + var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); + + Assert.Equal(4, result.Destinations.Count); + Assert.Collection(result.Destinations.Values, + a => + { + Assert.Equal("https://localhost:1111/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://127.0.0.1:2222/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://[::1]:3333/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + Assert.Null(a.Host); + } + }, + a => + { + Assert.Equal("https://baskets-galore.faketld/", a.Address); + if (configHasHost) + { + Assert.Equal("my-basket-svc.faketld", a.Host); + } + else + { + // For non-localhost values, fallback to the input address. + Assert.Equal("basket", a.Host); + } + }); + } + [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Configuration_DisallowedScheme() { @@ -125,8 +213,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo }) .AddConfigurationServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -149,8 +236,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { @@ -209,8 +295,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); - var coreResolver = services.GetRequiredService(); - var yarpResolver = new ServiceDiscoveryDestinationResolver(coreResolver, services.GetRequiredService>()); + var yarpResolver = CreateResolver(services); var destinationConfigs = new Dictionary { From 74cfe110cac6d8dd63e18891d2d645a829664f00 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Wed, 12 Jun 2024 11:24:23 -0700 Subject: [PATCH 54/77] Mark shipped public api as such and add infrastructure for PublicApiAnalyzers (#4467) * Add Microsoft.CodeAnalysis.PublicApiAnalyzers Marking the API we shipped stable as Shipped * Add new API added in main to Unshipped lists * Adding more new API as unshipped * Enable Package Validation on all packages to prevent breaking changes * Update Versions.props Co-authored-by: Igor Velikorossov * Add PR suggestion --------- Co-authored-by: Igor Velikorossov --- .../PublicAPI.Shipped.txt | 27 ++++++++++++++++ .../PublicAPI.Unshipped.txt | 28 +--------------- .../PublicAPI.Shipped.txt | 31 ++++++++++++++++++ .../PublicAPI.Unshipped.txt | 32 +------------------ .../PublicAPI.Shipped.txt | 4 +++ .../PublicAPI.Unshipped.txt | 5 +-- .../PublicAPI.Shipped.txt | 29 +++++++++++++++++ .../PublicAPI.Unshipped.txt | 30 +---------------- 8 files changed, 95 insertions(+), 91 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt index 7dc5c58110b..b55e2b696ec 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt @@ -1 +1,28 @@ #nullable enable +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! +abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature +Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory +Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint +Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? +override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! +static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt index b55e2b696ec..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt @@ -1,28 +1,2 @@ #nullable enable -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt index 7dc5c58110b..aa1fee77235 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt @@ -1 +1,32 @@ #nullable enable +Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt index aa1fee77235..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt @@ -1,32 +1,2 @@ #nullable enable -Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt index 7dc5c58110b..55d92fd4caa 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt @@ -1 +1,5 @@ #nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt index 55d92fd4caa..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt @@ -1,5 +1,2 @@ #nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt index 7dc5c58110b..b3be4048a2d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt @@ -1 +1,30 @@ #nullable enable +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions +Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! +Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory +Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void +Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask +Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt index b3be4048a2d..074c6ad103b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt @@ -1,30 +1,2 @@ #nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! + From d29308cad06d4933a6c005a5d4ffc1ec664ec514 Mon Sep 17 00:00:00 2001 From: Igor Velikorossov Date: Tue, 18 Jun 2024 16:56:25 +1000 Subject: [PATCH 55/77] Baseline code coverage (#4313) Resolves #4293 --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 4 ++++ .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index e98dc409e76..f0edb07ec01 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -9,6 +9,10 @@ Microsoft.Extensions.ServiceDiscovery + + 82 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 0d4c3cbeac2..c49ed456b30 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,6 +8,10 @@ $(DefaultDotnetIconFullPath) + + 51 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 08f6394daf8..04154ce9f9b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -10,6 +10,10 @@ $(DefaultDotnetIconFullPath) + + 72 + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 9a5d67db04e..69aaefc1075 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -8,6 +8,10 @@ $(DefaultDotnetIconFullPath) + + 81 + + From cb6df3de2ea980c59f7e054ffa72cb41872fff44 Mon Sep 17 00:00:00 2001 From: Valentin Hamm <88094233+vha-schleupen@users.noreply.github.com> Date: Mon, 5 Aug 2024 19:08:26 +0200 Subject: [PATCH 56/77] Filter additional records from DNS SRV response (#4463) --- .../DnsSrvServiceEndpointProvider.cs | 2 +- .../DnsSrvServiceEndpointResolverTests.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index dd17a7e2732..c174cda4f68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -42,7 +42,7 @@ protected override async Task ResolveAsyncCore() } var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals) + foreach (var record in result.Additionals.Where(x => x is AddressRecord or CNameRecord)) { ttl = MinTtl(record, ttl); lookupMapping[record.DomainName] = record; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 3449c075047..0a6e27974db 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -91,7 +91,8 @@ public async Task ResolveServiceEndpoint_DnsSrv() { new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), + new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) } }; @@ -152,7 +153,8 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( { new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")) + new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), + new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) } }; From 037ae58abb07590890ffb4804601bc2c9f3e08e7 Mon Sep 17 00:00:00 2001 From: ZLoo Date: Fri, 9 Aug 2024 20:41:26 +0300 Subject: [PATCH 57/77] Adding public API test coverage (#5225) --- ...DiscoveryDnsServiceCollectionExtensions.cs | 20 ++++- .../DnsServicePublicApiTests.cs | 81 +++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 17544d09486..98b9de1fd68 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -23,7 +23,12 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// DNS SRV queries are able to provide port numbers for endpoints and can support multiple named endpoints per service. /// However, not all environment support DNS SRV queries, and in some environments, additional configuration may be required. /// - public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) => services.AddDnsSrvServiceEndpointProvider(_ => { }); + public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + return services.AddDnsSrvServiceEndpointProvider(_ => { }); + } /// /// Adds DNS SRV service discovery to the . @@ -37,6 +42,9 @@ public static class ServiceDiscoveryDnsServiceCollectionExtensions /// public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.TryAddSingleton(); services.AddSingleton(); @@ -53,7 +61,12 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC /// /// DNS A/AAAA queries are widely available but are not able to provide port numbers for endpoints and cannot support multiple named endpoints per service. /// - public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) => services.AddDnsServiceEndpointProvider(_ => { }); + public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(services); + + return services.AddDnsServiceEndpointProvider(_ => { }); + } /// /// Adds DNS service discovery to the . @@ -66,6 +79,9 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC /// public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.AddSingleton(); var options = services.AddOptions(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs new file mode 100644 index 00000000000..e347deb9822 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs @@ -0,0 +1,81 @@ +// 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.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class DnsServicePublicApiTests +{ + [Fact] + public void AddDnsSrvServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddDnsSrvServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddDnsSrvServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + IServiceCollection services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddDnsSrvServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddDnsServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + IServiceCollection services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } +} From 204a929fc7f94d2c289c2d31d65120294e3f3234 Mon Sep 17 00:00:00 2001 From: ZLoo Date: Wed, 14 Aug 2024 10:43:19 +0300 Subject: [PATCH 58/77] Adding public API test coverage for Microsoft.Extensions.ServiceDiscovery.Yarp --- ...ReverseProxyServiceCollectionExtensions.cs | 6 +++ .../YarpServiceDiscoveryPublicApiTests.cs | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs index 9f473fd3a9b..de74dc0fc24 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryReverseProxyServiceCollectionExtensions.cs @@ -17,6 +17,8 @@ public static class ServiceDiscoveryReverseProxyServiceCollectionExtensions /// public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { + ArgumentNullException.ThrowIfNull(builder); + builder.Services.AddServiceDiscoveryCore(); builder.Services.AddSingleton(); return builder; @@ -27,6 +29,8 @@ public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this I /// public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + return services.AddHttpForwarder().AddServiceDiscoveryForwarderFactory(); } @@ -35,6 +39,8 @@ public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServ /// public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + services.AddServiceDiscoveryCore(); services.AddSingleton(); return services; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs new file mode 100644 index 00000000000..a3b694c6d70 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryPublicApiTests.cs @@ -0,0 +1,45 @@ +// 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.DependencyInjection; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; + +#pragma warning disable IDE0200 + +public class YarpServiceDiscoveryPublicApiTests +{ + [Fact] + public void AddServiceDiscoveryDestinationResolverShouldThrowWhenBuilderIsNull() + { + IReverseProxyBuilder builder = null!; + + var action = () => builder.AddServiceDiscoveryDestinationResolver(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(builder), exception.ParamName); + } + + [Fact] + public void AddHttpForwarderWithServiceDiscoveryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddHttpForwarderWithServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryForwarderFactoryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscoveryForwarderFactory(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } +} From a53b7ba2663df3e21b2e7df438e7ad9306d65fd0 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Mon, 9 Sep 2024 10:36:47 +1000 Subject: [PATCH 59/77] use static for classes with all static members (#5485) --- .../Configuration/ConfigurationServiceEndpointProvider.Log.cs | 2 +- .../PassThrough/PassThroughServiceEndpointProvider.Log.cs | 2 +- .../ServiceEndpointWatcher.Log.cs | 2 +- .../ServiceEndpointWatcherFactory.Log.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs index 48c922a85b3..b27c5ea9190 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.Log.cs @@ -8,7 +8,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Configuration; internal sealed partial class ConfigurationServiceEndpointProvider { - private sealed partial class Log + private static 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); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs index f9a984cfe4f..9f6e9ce0ccb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PassThrough/PassThroughServiceEndpointProvider.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.PassThrough; internal sealed partial class PassThroughServiceEndpointProvider { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Using pass-through service endpoint provider for service '{ServiceName}'.", EventName = "UsingPassThrough")] internal static partial void UsingPassThrough(ILogger logger, string serviceName); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs index fce9f667b40..8acaa55ee73 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; partial class ServiceEndpointWatcher { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Resolving endpoints for service '{ServiceName}'.", EventName = "ResolvingEndpoints")] public static partial void ResolvingEndpoints(ILogger logger, string serviceName); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs index 5f4acc89874..449ee6920de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcherFactory.Log.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.ServiceDiscovery; partial class ServiceEndpointWatcherFactory { - private sealed partial class Log + private static partial class Log { [LoggerMessage(1, LogLevel.Debug, "Creating endpoint resolver for service '{ServiceName}' with {Count} providers: {Providers}.", EventName = "CreatingResolver")] public static partial void ServiceEndpointProviderListCore(ILogger logger, string serviceName, int count, string providers); From 3294486990cd4f26a55338f6f1faf4ab6f10f6d7 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Wed, 11 Sep 2024 14:10:43 -0400 Subject: [PATCH 60/77] Upgrade tooling for 9.0x (#5483) * Update dependencies from ".NET 9 Eng" channel Updating 'Microsoft.SourceBuild.Intermediate.source-build-reference-packages': '8.0.0-alpha.1.23516.4' => '9.0.0-alpha.1.24453.2' (from build '20240903.2' of 'https://github.com/dotnet/source-build-reference-packages') Updating 'Microsoft.SourceBuild.Intermediate.arcade': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Arcade.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Build.Tasks.Installers': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Build.Tasks.Workloads': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.Helix.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.RemoteExecutor': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.SharedFramework.Sdk': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') Updating 'Microsoft.DotNet.XUnitExtensions': '8.0.0-beta.24426.2' => '9.0.0-beta.24453.1' (from build '20240903.1' of 'https://github.com/dotnet/arcade') * Add dotnet 8 versions for testing * [tests] Install 8.0 runtime for workload testing * Resolve xUnit2013 warning `Do not use Assert.Equal() to check for collection size. Use Assert.Single instead.xUnit2013` * Resolve xUnit2029 warning `Do not use Assert.Empty() to check if a value does not exist in a collection. Use Assert.DoesNotContain() instead.xUnit2029` * Track API change in EqualException * Ignore warning xUnit1030 `error xUnit1030: Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)` * Fix xUnit1012 analyzer warning. Warning of the form: `error xUnit1012: Null should not be used for type parameter 'secondaryApiKey' of type 'string'. Use a non-null value, or convert the parameter to a nullable type. (https://xunit.net/xunit.analyzers/rules/xUnit1012)` * Fix compile error. `error CS0121: The call is ambiguous between the following methods or properties: 'Assert.Contains(T, HashSet)' and 'Assert.Contains(T, SortedSet)'` * couple more fixes * Update nuget.config to use dotnet9 sources instead of dotnet8 * Remove unnecessary package reference to Microsoft.DotNet.XunitAssert * Address xUnit1030 warning. `error xUnit1030: Test methods should not call ConfigureAwait(false), as it may bypass parallelization limits. Omit ConfigureAwait, or use ConfigureAwait(true) to avoid CA2007. (https://xunit.net/xunit.analyzers/rules/xUnit1030)` * Ignore some lint warnings for eng/common/template-guidance.md from arcade * cleanup * Fix AzureFunctionsEndToEnd * Track changes for Aspire.Hosting.Azure.Functions * Fix CA2007: src\Aspire.Hosting\Health\ResourceHealthCheckScheduler.cs(38,45): error CA2007: Consider calling ConfigureAwait on the awaited task * Add dependency to Microsoft.DotNet.XliffTasks from dotnet/arcade * Add back skipping hosting.sdk, and projectTemplates packages .. in workload testing as it is required for the internal build. `Aspire.Hosting.Sdk.Msi.arm64.9.0.0-preview.4.24460.2.nupkg, Aspire.Hosting.Sdk.Msi.x64.9.0.0-preview.4.24460.2.nupkg, Aspire.Hosting.Sdk.Msi.x86.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.arm64.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.x64.9.0.0-preview.4.24460.2.nupkg, Aspire.ProjectTemplates.Msi.x86.9.0.0-preview.4.24460.2.nupkg` * [tests] Increase the default timeout from 10mins to 15mins for basictests .. like the various hosting integration tests. The run time can vary especially if docker needs to fetch an image. --- ...sions.ServiceDiscovery.Abstractions.csproj | 2 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- ...crosoft.Extensions.ServiceDiscovery.csproj | 2 +- .../DnsSrvServiceEndpointResolverTests.cs | 4 +-- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 2 +- ...nfigurationServiceEndpointResolverTests.cs | 34 +++++++++---------- ...t.Extensions.ServiceDiscovery.Tests.csproj | 2 +- ...PassThroughServiceEndpointResolverTests.cs | 6 ++-- .../ServiceEndpointResolverTests.cs | 22 ++++++------ ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 2 +- .../YarpServiceDiscoveryTests.cs | 8 ++--- 12 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index f0edb07ec01..733a192e93c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index c49ed456b30..3f503049e83 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 04154ce9f9b..a29425781b2 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable true diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 69aaefc1075..a9c4eaa1f8c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) true true Provides extensions to HttpClient that enable service discovery based on configuration. diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index 0a6e27974db..b58d9e2f4ec 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -112,7 +112,7 @@ public async Task ResolveServiceEndpoint_DnsSrv() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); @@ -199,7 +199,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.Null(initialResult.Exception); Assert.True(initialResult.ResolvedSuccessfully); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 31fe0f9d687..38a313a9249 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs index 6955cc1e8e2..9fc8832fa68 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ConfigurationServiceEndpointResolverTests.cs @@ -37,7 +37,7 @@ public async Task ResolveServiceEndpoint_Configuration_SingleResult_NoScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -81,7 +81,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Empty(initialResult.EndpointSource.Endpoints); @@ -95,7 +95,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -110,7 +110,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -125,7 +125,7 @@ public async Task ResolveServiceEndpoint_Configuration_DisallowedScheme() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -165,10 +165,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => @@ -187,10 +187,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } @@ -202,10 +202,10 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri("https://localhost:8080")), initialResult.EndpointSource.Endpoints[0].EndPoint); } } @@ -256,12 +256,12 @@ public async Task ResolveServiceEndpoint_Configuration_DefaultEndpointName_Resol var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); if (expectedResult is not null) { - Assert.Equal(1, initialResult.EndpointSource.Endpoints.Count); + Assert.Single(initialResult.EndpointSource.Endpoints); Assert.Equal(new UriEndPoint(new Uri(expectedResult)), initialResult.EndpointSource.Endpoints[0].EndPoint); } else @@ -296,7 +296,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); @@ -318,7 +318,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleResults() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(2, initialResult.EndpointSource.Endpoints.Count); @@ -363,7 +363,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); @@ -408,7 +408,7 @@ public async Task ResolveServiceEndpoint_Configuration_MultipleProtocols_WithSpe var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); Assert.Equal(3, initialResult.EndpointSource.Endpoints.Count); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 73bc9ec1eab..9fb61145211 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs index e0af5c03ed4..f8cc2f282e1 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/PassThroughServiceEndpointResolverTests.cs @@ -32,7 +32,7 @@ public async Task ResolveServiceEndpoint_PassThrough() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); var ep = Assert.Single(initialResult.EndpointSource.Endpoints); @@ -63,7 +63,7 @@ public async Task ResolveServiceEndpoint_Superseded() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); @@ -96,7 +96,7 @@ public async Task ResolveServiceEndpoint_Fallback() var tcs = new TaskCompletionSource(); watcher.OnEndpointsUpdated = tcs.SetResult; watcher.Start(); - var initialResult = await tcs.Task.ConfigureAwait(false); + var initialResult = await tcs.Task; Assert.NotNull(initialResult); Assert.True(initialResult.ResolvedSuccessfully); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index 16950e67374..0e08c07271e 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -109,7 +109,7 @@ public async Task ResolveServiceEndpoint() await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); - var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); + var initialResult = await watcher.GetEndpointsAsync(CancellationToken.None); Assert.NotNull(initialResult); var sep = Assert.Single(initialResult.Endpoints); var ip = Assert.IsType(sep.EndPoint); @@ -121,7 +121,7 @@ public async Task ResolveServiceEndpoint() Assert.False(tcs.Task.IsCompleted); cts[0].Cancel(); - var resolverResult = await tcs.Task.ConfigureAwait(false); + var resolverResult = await tcs.Task; Assert.NotNull(resolverResult); Assert.True(resolverResult.ResolvedSuccessfully); Assert.Equal(2, resolverResult.EndpointSource.Endpoints.Count); @@ -158,14 +158,14 @@ public async Task ResolveServiceEndpointOneShot() var resolver = services.GetRequiredService(); Assert.NotNull(resolver); - var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None).ConfigureAwait(false); + var initialResult = await resolver.GetEndpointsAsync("http://basket", CancellationToken.None); 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); - await services.DisposeAsync().ConfigureAwait(false); + await services.DisposeAsync(); } [Fact] @@ -196,13 +196,13 @@ public async Task ResolveHttpServiceEndpointOneShot() Assert.NotNull(resolver); var httpRequest = new HttpRequestMessage(HttpMethod.Get, "http://basket"); - var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None).ConfigureAwait(false); + var endpoint = await resolver.GetEndpointAsync(httpRequest, CancellationToken.None); Assert.NotNull(endpoint); var ip = Assert.IsType(endpoint.EndPoint); Assert.Equal(IPAddress.Parse("127.1.1.1"), ip.Address); Assert.Equal(8080, ip.Port); - await services.DisposeAsync().ConfigureAwait(false); + await services.DisposeAsync(); } [Fact] @@ -242,7 +242,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() await using ((watcher = watcherFactory.CreateWatcher("http://basket")).ConfigureAwait(false)) { Assert.NotNull(watcher); - var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None).ConfigureAwait(false); + var initialEndpointsTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); var initialEndpoints = await initialEndpointsTask; Assert.NotNull(initialEndpoints); @@ -257,7 +257,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); await resolveTask.ConfigureAwait(false); - }).ConfigureAwait(false); + }); Assert.Equal("throwing", exception.Message); @@ -269,8 +269,8 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() cts[0].Cancel(); sem.Release(1); var resolveTask = watcher.GetEndpointsAsync(CancellationToken.None); - await resolveTask.ConfigureAwait(false); - var next = await channel.Reader.ReadAsync(CancellationToken.None).ConfigureAwait(false); + await resolveTask; + var next = await channel.Reader.ReadAsync(CancellationToken.None); if (next.ResolvedSuccessfully) { break; @@ -279,7 +279,7 @@ public async Task ResolveServiceEndpoint_ThrowOnReload() var task = watcher.GetEndpointsAsync(CancellationToken.None); sem.Release(1); - var result = await task.ConfigureAwait(false); + var result = await task; Assert.NotSame(initialEndpoints, result); var sep = Assert.Single(result.Endpoints); var ip = Assert.IsType(sep.EndPoint); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 8a816222c9f..567c2254b81 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -1,7 +1,7 @@ - $(NetCurrent) + $(DefaultTargetFramework) enable enable diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index f2264e46411..5efb4a98c2c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -45,7 +45,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_PassThrough() var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("https://my-svc/", a)); } @@ -76,7 +76,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration() var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("https://localhost:8888/", a)); } @@ -106,7 +106,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_NonPref var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); - Assert.Equal(1, result.Destinations.Count); + Assert.Single(result.Destinations); Assert.Collection(result.Destinations.Select(d => d.Value.Address), a => Assert.Equal("http://localhost:1111/", a)); } @@ -226,7 +226,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo var result = await yarpResolver.ResolveDestinationsAsync(destinationConfigs, CancellationToken.None); // No results: there are no 'https' endpoints in config and 'http' is disallowed. - Assert.Equal(0, result.Destinations.Count); + Assert.Empty(result.Destinations); } [Fact] From a8c389b01f045ca9569720c43dfd745132c6fca5 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Thu, 5 Dec 2024 17:28:39 +0100 Subject: [PATCH 61/77] Do not set host if it's not explicitely set in config (#6862) --- .../ServiceDiscoveryDestinationResolver.cs | 15 +-------------- .../YarpServiceDiscoveryTests.cs | 3 +-- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs index 113e243565e..2ca456ec911 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/ServiceDiscoveryDestinationResolver.cs @@ -89,26 +89,13 @@ public async ValueTask ResolveDestinationsAsync(I } var name = $"{originalName}[{addressString}]"; - string? resolvedHost; + string? resolvedHost = null; // Use the configured 'Host' value if it is provided. if (!string.IsNullOrEmpty(originalConfig.Host)) { resolvedHost = originalConfig.Host; } - else if (uri.IsLoopback) - { - // If there is no configured 'Host' value and the address resolves to localhost, do not set a host. - // This is to account for non-wildcard development certificate. - resolvedHost = null; - } - else - { - // Excerpt from RFC 9110 Section 7.2: The "Host" header field in a request provides the host and port information from the target URI [...] - // See: https://www.rfc-editor.org/rfc/rfc9110.html#field.host - // i.e, use Authority and not Host. - resolvedHost = originalUri.Authority; - } var config = originalConfig with { Host = resolvedHost, Address = resolvedAddress, Health = healthAddress }; results.Add((name, config)); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 5efb4a98c2c..96fd46d47ea 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -188,8 +188,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Host_Va } else { - // For non-localhost values, fallback to the input address. - Assert.Equal("basket", a.Host); + Assert.Null(a.Host); } }); } From cb4a39bee2f86b70d14cbc4ae5e3cfba604e1795 Mon Sep 17 00:00:00 2001 From: Jose Perez Rodriguez Date: Sun, 2 Feb 2025 10:16:55 -0800 Subject: [PATCH 62/77] Adding workflow to automatically compare public API surface against previous release (#7369) * Add generate-api-diffs workflow that sends PRs with updated API surface area * [create-pull-request] automated change --------- Co-authored-by: joperezr <13854455+joperezr@users.noreply.github.com> --- ...xtensions.ServiceDiscovery.Abstractions.cs | 71 +++++++++++++++++++ ...crosoft.Extensions.ServiceDiscovery.Dns.cs | 52 ++++++++++++++ ...rosoft.Extensions.ServiceDiscovery.Yarp.cs | 19 +++++ .../Microsoft.Extensions.ServiceDiscovery.cs | 68 ++++++++++++++++++ 4 files changed, 210 insertions(+) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs new file mode 100644 index 00000000000..a7ed4ec5404 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs @@ -0,0 +1,71 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.ServiceDiscovery +{ + public partial interface IHostNameFeature + { + string HostName { get; } + } + + public partial interface IServiceEndpointBuilder + { + System.Collections.Generic.IList Endpoints { get; } + + AspNetCore.Http.Features.IFeatureCollection Features { get; } + + void AddChangeToken(Primitives.IChangeToken changeToken); + } + + public partial interface IServiceEndpointProvider : System.IAsyncDisposable + { + System.Threading.Tasks.ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, System.Threading.CancellationToken cancellationToken); + } + + public partial interface IServiceEndpointProviderFactory + { + bool TryCreateProvider(ServiceEndpointQuery query, out IServiceEndpointProvider? provider); + } + + public abstract partial class ServiceEndpoint + { + public abstract System.Net.EndPoint EndPoint { get; } + public abstract AspNetCore.Http.Features.IFeatureCollection Features { get; } + + public static ServiceEndpoint Create(System.Net.EndPoint endPoint, AspNetCore.Http.Features.IFeatureCollection? features = null) { throw null; } + } + + public sealed partial class ServiceEndpointQuery + { + internal ServiceEndpointQuery() { } + + public string? EndpointName { get { throw null; } } + + public System.Collections.Generic.IReadOnlyList IncludedSchemes { get { throw null; } } + + public string ServiceName { get { throw null; } } + + public override string? ToString() { throw null; } + + public static bool TryParse(string input, out ServiceEndpointQuery? query) { throw null; } + } + + [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] + public sealed partial class ServiceEndpointSource + { + public ServiceEndpointSource(System.Collections.Generic.List? endpoints, Primitives.IChangeToken changeToken, AspNetCore.Http.Features.IFeatureCollection features) { } + + public Primitives.IChangeToken ChangeToken { get { throw null; } } + + public System.Collections.Generic.IReadOnlyList Endpoints { get { throw null; } } + + public AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } + + public override string ToString() { throw null; } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs new file mode 100644 index 00000000000..15f99b179ec --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs @@ -0,0 +1,52 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.Hosting +{ + public static partial class ServiceDiscoveryDnsServiceCollectionExtensions + { + public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } + + public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery.Dns +{ + public partial class DnsServiceEndpointProviderOptions + { + public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } + + public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } + + public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } + + public double RetryBackOffFactor { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } + + public partial class DnsSrvServiceEndpointProviderOptions + { + public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } + + public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } + + public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } + + public string? QuerySuffix { get { throw null; } set { } } + + public double RetryBackOffFactor { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs new file mode 100644 index 00000000000..fc608f86a92 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs @@ -0,0 +1,19 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class ServiceDiscoveryReverseProxyServiceCollectionExtensions + { + public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { throw null; } + + public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { throw null; } + + public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { throw null; } + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs new file mode 100644 index 00000000000..a6ba654085e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class ServiceDiscoveryHttpClientBuilderExtensions + { + public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { throw null; } + } + + public static partial class ServiceDiscoveryServiceCollectionExtensions + { + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { throw null; } + + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, System.Action configureOptions) { throw null; } + + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery +{ + public sealed partial class ConfigurationServiceEndpointProviderOptions + { + public string SectionName { get { throw null; } set { } } + + public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } + } + + public sealed partial class ServiceDiscoveryOptions + { + public bool AllowAllSchemes { get { throw null; } set { } } + + public System.Collections.Generic.IList AllowedSchemes { get { throw null; } set { } } + + public System.TimeSpan RefreshPeriod { get { throw null; } set { } } + } + + public sealed partial class ServiceEndpointResolver : System.IAsyncDisposable + { + internal ServiceEndpointResolver() { } + + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + + public System.Threading.Tasks.ValueTask GetEndpointsAsync(string serviceName, System.Threading.CancellationToken cancellationToken) { throw null; } + } +} + +namespace Microsoft.Extensions.ServiceDiscovery.Http +{ + public partial interface IServiceDiscoveryHttpMessageHandlerFactory + { + System.Net.Http.HttpMessageHandler CreateHandler(System.Net.Http.HttpMessageHandler handler); + } +} \ No newline at end of file From 763344cb20d62b0e429201e8cdc78eb2be94a66c Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 3 Feb 2025 15:08:59 -0600 Subject: [PATCH 63/77] Remove PublicApiAnalyzer (#7389) This is no longer necessary after #7369 --- .../PublicAPI.Shipped.txt | 28 ---------------- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 32 ------------------- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 5 --- .../PublicAPI.Unshipped.txt | 2 -- .../PublicAPI.Shipped.txt | 30 ----------------- .../PublicAPI.Unshipped.txt | 2 -- 8 files changed, 103 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt deleted file mode 100644 index b55e2b696ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Shipped.txt +++ /dev/null @@ -1,28 +0,0 @@ -#nullable enable -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.EndPoint.get -> System.Net.EndPoint! -abstract Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature -Microsoft.Extensions.ServiceDiscovery.IHostNameFeature.HostName.get -> string! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.AddChangeToken(Microsoft.Extensions.Primitives.IChangeToken! changeToken) -> void -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Endpoints.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider.PopulateAsync(Microsoft.Extensions.ServiceDiscovery.IServiceEndpointBuilder! endpoints, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory -Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProviderFactory.TryCreateProvider(Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery! query, out Microsoft.Extensions.ServiceDiscovery.IServiceEndpointProvider? provider) -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint -Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.ServiceEndpoint() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.EndpointName.get -> string? -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.IncludedSchemes.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ServiceName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ChangeToken.get -> Microsoft.Extensions.Primitives.IChangeToken! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Endpoints.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.Features.get -> Microsoft.AspNetCore.Http.Features.IFeatureCollection! -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ServiceEndpointSource(System.Collections.Generic.List? endpoints, Microsoft.Extensions.Primitives.IChangeToken! changeToken, Microsoft.AspNetCore.Http.Features.IFeatureCollection! features) -> void -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.ToString() -> string? -override Microsoft.Extensions.ServiceDiscovery.ServiceEndpointSource.ToString() -> string! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint.Create(System.Net.EndPoint! endPoint, Microsoft.AspNetCore.Http.Features.IFeatureCollection? features = null) -> Microsoft.Extensions.ServiceDiscovery.ServiceEndpoint! -static Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery.TryParse(string! input, out Microsoft.Extensions.ServiceDiscovery.ServiceEndpointQuery? query) -> bool diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt deleted file mode 100644 index aa1fee77235..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Shipped.txt +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable -Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.DnsServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DefaultRefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.DnsSrvServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MaxRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.MinRetryPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.get -> string? -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.QuerySuffix.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.get -> double -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.RetryBackOffFactor.set -> void -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.Dns.DnsSrvServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.Hosting.ServiceDiscoveryDnsServiceCollectionExtensions.AddDnsSrvServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt deleted file mode 100644 index 55d92fd4caa..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Shipped.txt +++ /dev/null @@ -1,5 +0,0 @@ -#nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddHttpForwarderWithServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryDestinationResolver(this Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! builder) -> Microsoft.Extensions.DependencyInjection.IReverseProxyBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryReverseProxyServiceCollectionExtensions.AddServiceDiscoveryForwarderFactory(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt deleted file mode 100644 index b3be4048a2d..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Shipped.txt +++ /dev/null @@ -1,30 +0,0 @@ -#nullable enable -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions -Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ConfigurationServiceEndpointProviderOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.get -> string! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.SectionName.set -> void -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.get -> System.Func! -Microsoft.Extensions.ServiceDiscovery.ConfigurationServiceEndpointProviderOptions.ShouldApplyHostNameMetadata.set -> void -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory -Microsoft.Extensions.ServiceDiscovery.Http.IServiceDiscoveryHttpMessageHandlerFactory.CreateHandler(System.Net.Http.HttpMessageHandler! handler) -> System.Net.Http.HttpMessageHandler! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.get -> bool -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowAllSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.get -> System.Collections.Generic.IList! -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.AllowedSchemes.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.get -> System.TimeSpan -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.RefreshPeriod.set -> void -Microsoft.Extensions.ServiceDiscovery.ServiceDiscoveryOptions.ServiceDiscoveryOptions() -> void -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.DisposeAsync() -> System.Threading.Tasks.ValueTask -Microsoft.Extensions.ServiceDiscovery.ServiceEndpointResolver.GetEndpointsAsync(string! serviceName, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryHttpClientBuilderExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! httpClientBuilder) -> Microsoft.Extensions.DependencyInjection.IHttpClientBuilder! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddConfigurationServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddPassThroughServiceEndpointProvider(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscovery(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.ServiceDiscoveryServiceCollectionExtensions.AddServiceDiscoveryCore(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt deleted file mode 100644 index 074c6ad103b..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,2 +0,0 @@ -#nullable enable - From 9a34a4584a331f4fb9295117a2cb7efb09fc96db Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 3 Mar 2025 13:41:46 -0500 Subject: [PATCH 64/77] [ci] Remove code coverage reporting from the pipeline (#7857) * [ci] Remove codecoverage from the pipeline as it is not being used * Remove unneeded MinCodeCoverage property * Remove ProjectStaging.targets * remove more code coverage report references * fix build * address review feedback from @ eerhardt --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 4 ---- .../Microsoft.Extensions.ServiceDiscovery.csproj | 4 ---- 4 files changed, 16 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 733a192e93c..6f412779689 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -9,10 +9,6 @@ Microsoft.Extensions.ServiceDiscovery - - 82 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 3f503049e83..9854c93c01a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - 51 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index a29425781b2..74870a87668 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -10,10 +10,6 @@ $(DefaultDotnetIconFullPath) - - 72 - - diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index a9c4eaa1f8c..59c1c31fee9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -8,10 +8,6 @@ $(DefaultDotnetIconFullPath) - - 81 - - From ec982ceeaca866ec8be110157470c2063cddf30a Mon Sep 17 00:00:00 2001 From: ZLoo Date: Tue, 4 Mar 2025 07:02:28 +0300 Subject: [PATCH 65/77] Adding test coverage - validate arguments of public methods (#7575) * update qdrant public api tests * update redis public api tests * update python public api tests * update testing public api tests * update sql server public api tests * update rabbitMQ public api tests * update postgre sql public api tests * update nodeJs public api tests * update nats public api tests * update my sql public api tests * update mongo db public api tests * update milvus public api tests * update keycloak public api tests * update kafka public api tests * update garnet public api tests * update elasticsearch public api tests * add azure aI open aI public api tests * update azure data tables public api tests * update messaging event hub public api tests * update messaging service bus public api tests * update messaging web pub sub public api tests * update search documents public api tests * update security key vault public api tests * update storage blobs public api tests * update storage queues public api test * update confluent kafka public api test * update elastic clients elasticsearch public api test * update keycloack authentication public api test * update microsoft azure cosmos public api tests * change HostApplicationBuilder to Host.CreateEmptyApplicationBuilder * change IHostApplicationBuilder to var * update microsoft data sql client public api tests * update microsoft entity framework core cosmos public api tests * update microsoft entity framework core sql server public api tests * update milvus client public api tests * update mongo db driver public api tests * update my sql connector public api tests * update nats net public api tests * update confluent kafka public api tests * update npgsql entity framework core postgre sql public api tests * update npgsql public api tests * update open ai public api tests * update oracle entity framework core public api tests * update pomelo entity framework core my sql public api tests * update qdrant client public api tests * update rabbit mq client public api tests * update seq public api tests * update stack exchange redis distributed caching public api tests * update stack exchange redis output caching public api tests * update stack exchang redis public api tests * update extensions service discovery public api tests * fix python tests * update oracle public api tests * update valkey public api tests * update app configuration, app containers, application insights, cognitive services, cosmos db public api tests * add Aspire.Hosting.Azure.Tests * fix duplicate * Fix MR by feedback --- .../ServiceEndpoint.cs | 7 +- .../ServiceEndpointQuery.cs | 2 + .../ServiceEndpointSource.cs | 1 + ...iceDiscoveryHttpClientBuilderExtensions.cs | 2 + ...iceDiscoveryServiceCollectionExtensions.cs | 22 +- .../ExtensionsServicePublicApiTests.cs | 218 ++++++++++++++++++ 6 files changed, 241 insertions(+), 11 deletions(-) create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs index 238e383a957..33e0eff4d69 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs @@ -28,5 +28,10 @@ public abstract class ServiceEndpoint /// The endpoint being represented. /// Features of the endpoint. /// A newly initialized . - public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) => new ServiceEndpointImpl(endPoint, features); + public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? features = null) + { + ArgumentNullException.ThrowIfNull(endPoint); + + return new ServiceEndpointImpl(endPoint, features); + } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 600dc5cc28c..20a17d0878f 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -35,6 +35,8 @@ private ServiceEndpointQuery(string originalString, string[] includedSchemes, st /// if the value was successfully parsed; otherwise . public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpointQuery? query) { + ArgumentException.ThrowIfNullOrEmpty(input); + bool hasScheme; if (!input.Contains("://", StringComparison.InvariantCulture) && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs index fb5bff1b288..28d987a2f34 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointSource.cs @@ -25,6 +25,7 @@ public sealed class ServiceEndpointSource public ServiceEndpointSource(List? endpoints, IChangeToken changeToken, IFeatureCollection features) { ArgumentNullException.ThrowIfNull(changeToken); + ArgumentNullException.ThrowIfNull(features); _endpoints = endpoints; Features = features; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index 8a137aad4f8..7d5b94c10c5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -21,6 +21,8 @@ public static class ServiceDiscoveryHttpClientBuilderExtensions /// The builder. public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { + ArgumentNullException.ThrowIfNull(httpClientBuilder); + var services = httpClientBuilder.Services; services.AddServiceDiscoveryCore(); httpClientBuilder.AddHttpMessageHandler(services => diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs index a5d789b7e4e..8de759af1f6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryServiceCollectionExtensions.cs @@ -24,11 +24,9 @@ public static class ServiceDiscoveryServiceCollectionExtensions /// The service collection. /// The service collection. public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) - { - return services.AddServiceDiscoveryCore() + => AddServiceDiscoveryCore(services) .AddConfigurationServiceEndpointProvider() .AddPassThroughServiceEndpointProvider(); - } /// /// Adds the core service discovery services and configures defaults. @@ -37,18 +35,16 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// 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) + => AddServiceDiscoveryCore(services, configureOptions: configureOptions) .AddConfigurationServiceEndpointProvider() .AddPassThroughServiceEndpointProvider(); - } /// /// Adds the core service discovery services. /// /// The service collection. /// The service collection. - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => services.AddServiceDiscoveryCore(configureOptions: _ => { }); + public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) => AddServiceDiscoveryCore(services, configureOptions: _ => { }); /// /// Adds the core service discovery services. @@ -58,6 +54,9 @@ public static IServiceCollection AddServiceDiscovery(this IServiceCollection ser /// The service collection. public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddOptions(); services.AddLogging(); services.TryAddTransient, ServiceDiscoveryOptionsValidator>(); @@ -80,9 +79,7 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection /// The service collection. /// The service collection. public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) - { - return services.AddConfigurationServiceEndpointProvider(configureOptions: _ => { }); - } + => AddConfigurationServiceEndpointProvider(services, configureOptions: _ => { }); /// /// Configures a service discovery endpoint provider which uses to resolve endpoints. @@ -92,6 +89,9 @@ public static IServiceCollection AddConfigurationServiceEndpointProvider(this IS /// The service collection. public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, Action configureOptions) { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + services.AddServiceDiscoveryCore(); services.AddSingleton(); services.AddTransient, ConfigurationServiceEndpointProviderOptionsValidator>(); @@ -110,6 +110,8 @@ public static IServiceCollection AddConfigurationServiceEndpointProvider(this IS /// The service collection. public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { + ArgumentNullException.ThrowIfNull(services); + services.AddServiceDiscoveryCore(); services.AddSingleton(); return services; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs new file mode 100644 index 00000000000..31781cf6722 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ExtensionsServicePublicApiTests.cs @@ -0,0 +1,218 @@ +// 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; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Tests; + +#pragma warning disable IDE0200 + +public class ExtensionsServicePublicApiTests +{ + [Fact] + public void AddServiceDiscoveryShouldThrowWhenHttpClientBuilderIsNull() + { + IHttpClientBuilder httpClientBuilder = null!; + + var action = () => httpClientBuilder.AddServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(httpClientBuilder), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscovery(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddServiceDiscovery(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddServiceDiscovery(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddServiceDiscoveryCore(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddServiceDiscoveryCore(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddServiceDiscoveryCoreWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddServiceDiscoveryCore(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddConfigurationServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + Action configureOptions = (_) => { }; + + var action = () => services.AddConfigurationServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void AddConfigurationServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + { + var services = new ServiceCollection(); + Action configureOptions = null!; + + var action = () => services.AddConfigurationServiceEndpointProvider(configureOptions); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(configureOptions), exception.ParamName); + } + + [Fact] + public void AddPassThroughServiceEndpointProviderShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.AddPassThroughServiceEndpointProvider(); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public async Task GetEndpointsAsyncShouldThrowWhenServiceNameIsNull() + { + var serviceEndpointWatcherFactory = new ServiceEndpointWatcherFactory( + new List(), + new Logger(new NullLoggerFactory()), + Options.Options.Create(new ServiceDiscoveryOptions()), + TimeProvider.System); + + var serviceEndpointResolver = new ServiceEndpointResolver(serviceEndpointWatcherFactory, TimeProvider.System); + string serviceName = null!; + + var action = async () => await serviceEndpointResolver.GetEndpointsAsync(serviceName, CancellationToken.None); + + var exception = await Assert.ThrowsAsync(action); + Assert.Equal(nameof(serviceName), exception.ParamName); + } + + [Fact] + public void CreateShouldThrowWhenEndPointIsNull() + { + EndPoint endPoint = null!; + + var action = () => ServiceEndpoint.Create(endPoint); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(endPoint), exception.ParamName); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TryParseShouldThrowWhenEndPointIsNullOrEmpty(bool isNull) + { + var input = isNull ? null! : string.Empty; + + var action = () => + { + _ = ServiceEndpointQuery.TryParse(input, out _); + }; + + var exception = isNull + ? Assert.Throws(action) + : Assert.Throws(action); + Assert.Equal(nameof(input), exception.ParamName); + } + + [Fact] + public void CtorServiceEndpointSourceShouldThrowWhenChangeTokenIsNull() + { + IChangeToken changeToken = null!; + var features = new FeatureCollection(); + List? endpoints = null; + + var action = () => new ServiceEndpointSource(endpoints, changeToken, features); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(changeToken), exception.ParamName); + } + + [Fact] + public void CtorServiceEndpointSourceShouldThrowWhenFeaturesIsNull() + { + var changeToken = NullChangeToken.Singleton; + IFeatureCollection features = null!; + List? endpoints = null; + + var action = () => new ServiceEndpointSource(endpoints, changeToken, features); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(features), exception.ParamName); + } +} From 45867cee592a77eac08db8f94cb52cb965299f23 Mon Sep 17 00:00:00 2001 From: Youssef Victor Date: Wed, 2 Apr 2025 08:17:13 +0200 Subject: [PATCH 66/77] Migrate to xunit.v3 (#8403) * Update XUnit extensions from Arcade * Update packages and Arcade test runner * Move custom attributes to xunit.v3 * Fix usings * Move from ConditionalFact/ConditionalTheory to Assert.Skip * Suppress xUnit1030 * Remove unnecessary using * Few fixes * Exe * Param doc * Add OutputType * Adjust for helix * Produce binlog * Specify DOTNET_ROOT for executables to find the correct runtime * Fix build errors * Fix failing test This test used to pass with VSTest and xunit v2 because the tests were run under testhost.exe. So, DistributedApplicationOptions.Assembly was pointing to testhost. Then ResolveProjectDirectory wouldn't correctly find AppHostProjectPath. In DistributedApplicationBuilder constructor, we were falling back to _innerBuilder.Environment.ContentRootPath because ProjectDirectory is null. With xunit.v3 *or* MTP, generally when the test app is executed as normal executable, we have the right assembly and we are able to resolve project directory correctly * Fix for Arcade * Fix test * Move to props * Specify DOTNET_ROOT * Fix ExtractTestClassNames for local SDK usage as well * Set executable bit * Set xunit.analyzers version * Disable analyzer for now * Disable analyzer for now * Move to Directory.Build.props as NoWarn * Adjust * Disable more warnings * Suppress more warnings * Add back to props * fix merge * fix merge * fix merge * Remove unused properties for helix run * Disable xunit1051 from the project file for Playground, and Templates tests * add reference to issue * Update QuarantinedTestAttribute for xunit v3 * More fixes for merging main * Adjust for QuarantinedTest * More adjustments for merging main --------- Co-authored-by: Ankit Jain --- ...soft.Extensions.ServiceDiscovery.Dns.Tests.csproj | 2 +- ...icrosoft.Extensions.ServiceDiscovery.Tests.csproj | 2 +- .../ServiceEndpointTests.cs | 12 ++++++------ ...oft.Extensions.ServiceDiscovery.Yarp.Tests.csproj | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 38a313a9249..24faf1d8abe 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 9fb61145211..269081ae5d7 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs index e05d0818e1a..2943074c2b3 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointTests.cs @@ -10,16 +10,16 @@ public class ServiceEndpointTests { public static TheoryData ZeroPortEndPoints => new() { - IPEndPoint.Parse("127.0.0.1:0"), - new DnsEndPoint("microsoft.com", 0), - new UriEndPoint(new Uri("https://microsoft.com")) + (EndPoint)IPEndPoint.Parse("127.0.0.1:0"), + (EndPoint)new DnsEndPoint("microsoft.com", 0), + (EndPoint)new UriEndPoint(new Uri("https://microsoft.com")) }; public static TheoryData NonZeroPortEndPoints => new() { - IPEndPoint.Parse("127.0.0.1:8443"), - new DnsEndPoint("microsoft.com", 8443), - new UriEndPoint(new Uri("https://microsoft.com:8443")) + (EndPoint)IPEndPoint.Parse("127.0.0.1:8443"), + (EndPoint)new DnsEndPoint("microsoft.com", 8443), + (EndPoint)new UriEndPoint(new Uri("https://microsoft.com:8443")) }; [Theory] diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 567c2254b81..296a3dcd861 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -8,7 +8,7 @@ - + From f3feb150ba13400734336536a59983b256b2564c Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Tue, 29 Apr 2025 09:36:16 -0600 Subject: [PATCH 67/77] template updates for 9.3 (#8975) * remove 8.2 * 9.1 to 9.3 * remove 8.2 and 9.1 folders * Add 9.3 content * set concrete versions in 9.2 projects * 9.3 default * loc strings * update readmes for AppHost.cs * update tests for AppHost.cs --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 6c47ee67507..1073af1cc19 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -40,7 +40,7 @@ dotnet add package Microsoft.Extensions.ServiceDiscovery ### Usage example -In the _Program.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. +In the _AppHost.cs_ file of your project, call the `AddServiceDiscovery` extension method to add service discovery to the host, configuring default service endpoint providers. ```csharp builder.Services.AddServiceDiscovery(); From 397705a3c211d65ed67c269c7bb8e818cc0057af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Mon, 12 May 2025 10:32:23 -0700 Subject: [PATCH 68/77] Remove culture code from Microsoft documentation urls (#9253) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 1073af1cc19..71547ea9396 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -8,9 +8,9 @@ In typical systems, service configuration changes over time. Service discovery a Service discovery uses configured _providers_ to resolve service endpoints. When service endpoints are resolved, each registered provider is called in the order of registration to contribute to a collection of service endpoints (an instance of `ServiceEndpointSource`). -Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) system. +Providers implement the `IServiceEndpointProvider` interface. They are created by an instance of `IServiceEndpointProviderProvider`, which are registered with the [.NET dependency injection](https://learn.microsoft.com/dotnet/core/extensions/dependency-injection) system. -Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. +Developers typically add service discovery to their [`HttpClient`](https://learn.microsoft.com/dotnet/fundamentals/networking/http/httpclient) using the [`IHttpClientFactory`](https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory) with the `AddServiceDiscovery` extension method. Services can be resolved directly by calling `ServiceEndpointResolver`'s `GetEndpointsAsync` method, which returns a collection of resolved endpoints. From 83a203c50ba1105de911e96dcf0e0ae29e99d807 Mon Sep 17 00:00:00 2001 From: Tim Potze Date: Tue, 20 May 2025 23:47:05 +0200 Subject: [PATCH 69/77] Fix error in json in README.md (#9415) --- src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md index 71547ea9396..b767bb41e83 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/README.md @@ -189,7 +189,7 @@ With the configuration-based endpoint provider, named endpoints can be specified ```json { "Services": { - "basket": + "basket": { "https": "https://10.2.3.4:8080", /* the https endpoint, requested via https://basket */ "dashboard": "https://10.2.3.4:9999" /* the "dashboard" endpoint, requested via https://_dashboard.basket */ } From 8bf082f99b1b3e9b16545e17b41579b3c07fc6bb Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:58:10 +0200 Subject: [PATCH 70/77] Managed implementation of DNS resolver (#6104) This PR brings in a C# implementation of a DNS resolver that is able to signal the TTL information together with the query results. Main features Async network I/O, fully cancellable Mockable Resolves IP Addresses (A/AAAA records) and Service records (SRV + related A/AAAA) Transparent fallback to TCP Autodetection of OS settings (i.e. reads nameservers from /etc/resolv.conf file) Thread-safe --- .../DnsServiceEndpointProvider.cs | 35 +- .../DnsServiceEndpointProviderBase.cs | 2 +- .../DnsServiceEndpointProviderFactory.cs | 4 +- .../DnsSrvServiceEndpointProvider.cs | 53 +- .../DnsSrvServiceEndpointProviderFactory.cs | 10 +- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 5 +- .../Resolver/DnsDataReader.cs | 133 +++ .../Resolver/DnsDataWriter.cs | 121 +++ .../Resolver/DnsMessageHeader.cs | 36 + .../Resolver/DnsPrimitives.cs | 318 ++++++ .../Resolver/DnsResolver.Log.cs | 39 + .../Resolver/DnsResolver.Telemetry.cs | 115 +++ .../Resolver/DnsResolver.cs | 931 ++++++++++++++++++ .../Resolver/DnsResourceRecord.cs | 22 + .../Resolver/DnsResponse.cs | 39 + .../Resolver/EncodedDomainName.cs | 82 ++ .../Resolver/IDnsResolver.cs | 13 + .../Resolver/NetworkInfo.cs | 36 + .../Resolver/QueryClass.cs | 9 + .../Resolver/QueryFlags.cs | 15 + .../Resolver/QueryResponseCode.cs | 42 + .../Resolver/QueryType.cs | 55 ++ .../Resolver/ResolvConf.cs | 48 + .../Resolver/ResolverOptions.cs | 31 + .../Resolver/ResultTypes.cs | 10 + .../Resolver/SendQueryError.cs | 47 + ...DiscoveryDnsServiceCollectionExtensions.cs | 4 +- .../.gitignore | 2 + .../Fuzzers/DnsResponseFuzzer.cs | 44 + .../Fuzzers/EncodedDomainNameFuzzer.cs | 33 + .../Fuzzers/WriteDomainNameRoundTripFuzzer.cs | 48 + .../GlobalUsings.cs | 5 + .../IFuzzer.cs | 10 + ....ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 18 + .../Program.cs | 64 ++ .../DnsResponseFuzzer/ip-www.example.com | Bin 0 -> 141 bytes .../corpus-seed/DnsResponseFuzzer/name-error | Bin 0 -> 31 bytes .../DnsResponseFuzzer/name-error-2 | Bin 0 -> 91 bytes .../corpus-seed/DnsResponseFuzzer/no-data | Bin 0 -> 91 bytes .../DnsResponseFuzzer/server-error | Bin 0 -> 31 bytes .../ip-www.example.com | Bin 0 -> 143 bytes .../WriteDomainNameRoundTripFuzzer/example | 1 + .../WriteDomainNameRoundTripFuzzer/nonascii | 1 + .../WriteDomainNameRoundTripFuzzer/toolong | 1 + .../run.ps1 | 106 ++ .../DnsServiceEndpointResolverTests.cs | 2 + .../DnsSrvServiceEndpointResolverTests.cs | 123 +-- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 4 + .../Resolver/CancellationTests.cs | 42 + .../Resolver/DnsDataReaderTests.cs | 64 ++ .../Resolver/DnsDataWriterTests.cs | 148 +++ .../Resolver/DnsPrimitivesTests.cs | 195 ++++ .../Resolver/LoopbackDnsServer.cs | 331 +++++++ .../Resolver/LoopbackDnsTestBase.cs | 48 + .../Resolver/ResolvConfTests.cs | 26 + .../Resolver/ResolveAddressesTests.cs | 307 ++++++ .../Resolver/ResolveServiceTests.cs | 37 + .../Resolver/RetryTests.cs | 309 ++++++ .../Resolver/TcpFailoverTests.cs | 132 +++ .../YarpServiceDiscoveryTests.cs | 97 +- 60 files changed, 4233 insertions(+), 220 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/name-error create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/name-error-2 create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/no-data create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/server-error create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/EncodedDomainNameFuzzer/ip-www.example.com create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/example create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/nonascii create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/WriteDomainNameRoundTripFuzzer/toolong create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/run.ps1 create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs index 6cc9f92bc46..7a2d1b632e0 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProvider.cs @@ -4,6 +4,7 @@ using System.Net; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -12,6 +13,7 @@ internal sealed partial class DnsServiceEndpointProvider( string hostName, IOptionsMonitor options, ILogger logger, + IDnsResolver resolver, TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -29,17 +31,14 @@ protected override async Task ResolveAsyncCore() var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.AddressQuery(logger, ServiceName, hostName); - var addresses = await System.Net.Dns.GetHostAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + + var now = _timeProvider.GetUtcNow().DateTime; + var addresses = await resolver.ResolveIPAddressesAsync(hostName, ShutdownToken).ConfigureAwait(false); + foreach (var address in addresses) { - var serviceEndpoint = ServiceEndpoint.Create(new IPEndPoint(address, 0)); - serviceEndpoint.Features.Set(this); - if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) - { - serviceEndpoint.Features.Set(this); - } - - endpoints.Add(serviceEndpoint); + ttl = MinTtl(now, address.ExpiresAt, ttl); + endpoints.Add(CreateEndpoint(new IPEndPoint(address.Address, port: 0))); } if (endpoints.Count == 0) @@ -48,5 +47,23 @@ protected override async Task ResolveAsyncCore() } SetResult(endpoints, ttl); + + static TimeSpan MinTtl(DateTime now, DateTime expiresAt, TimeSpan existing) + { + var candidate = expiresAt - now; + return candidate < existing ? candidate : existing; + } + + ServiceEndpoint CreateEndpoint(EndPoint endPoint) + { + var serviceEndpoint = ServiceEndpoint.Create(endPoint); + serviceEndpoint.Features.Set(this); + if (options.CurrentValue.ShouldApplyHostNameMetadata(serviceEndpoint)) + { + serviceEndpoint.Features.Set(this); + } + + return serviceEndpoint; + } } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs index 6c69cc7a760..311c06f631a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderBase.cs @@ -14,7 +14,7 @@ internal abstract partial class DnsServiceEndpointProviderBase : IServiceEndpoin private readonly object _lock = new(); private readonly ILogger _logger; private readonly CancellationTokenSource _disposeCancellation = new(); - private readonly TimeProvider _timeProvider; + protected readonly TimeProvider _timeProvider; private long _lastRefreshTimeStamp; private Task _resolveTask = Task.CompletedTask; private bool _hasEndpoints; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs index c241ad89dd3..1da21411e64 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsServiceEndpointProviderFactory.cs @@ -4,18 +4,20 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsServiceEndpointProviderFactory( IOptionsMonitor options, ILogger logger, + IDnsResolver resolver, TimeProvider timeProvider) : IServiceEndpointProviderFactory { /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { - provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, timeProvider); + provider = new DnsServiceEndpointProvider(query, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs index c174cda4f68..6d5ade5059e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProvider.cs @@ -2,10 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using DnsClient; -using DnsClient.Protocol; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; @@ -15,7 +14,7 @@ internal sealed partial class DnsSrvServiceEndpointProvider( string hostName, IOptionsMonitor options, ILogger logger, - IDnsQuery dnsClient, + IDnsResolver resolver, TimeProvider timeProvider) : DnsServiceEndpointProviderBase(query, logger, timeProvider), IHostNameFeature { protected override double RetryBackOffFactor => options.CurrentValue.RetryBackOffFactor; @@ -35,56 +34,36 @@ protected override async Task ResolveAsyncCore() var endpoints = new List(); var ttl = DefaultRefreshPeriod; Log.SrvQuery(logger, ServiceName, srvQuery); - var result = await dnsClient.QueryAsync(srvQuery, QueryType.SRV, cancellationToken: ShutdownToken).ConfigureAwait(false); - if (result.HasError) - { - throw CreateException(srvQuery, result.ErrorMessage); - } - var lookupMapping = new Dictionary(); - foreach (var record in result.Additionals.Where(x => x is AddressRecord or CNameRecord)) - { - ttl = MinTtl(record, ttl); - lookupMapping[record.DomainName] = record; - } + var now = _timeProvider.GetUtcNow().DateTime; + var result = await resolver.ResolveServiceAsync(srvQuery, cancellationToken: ShutdownToken).ConfigureAwait(false); - var srvRecords = result.Answers.OfType(); - foreach (var record in srvRecords) + foreach (var record in result) { - if (!lookupMapping.TryGetValue(record.Target, out var targetRecord)) - { - continue; - } + ttl = MinTtl(now, record.ExpiresAt, ttl); - ttl = MinTtl(record, ttl); - if (targetRecord is AddressRecord addressRecord) + if (record.Addresses.Length > 0) { - endpoints.Add(CreateEndpoint(new IPEndPoint(addressRecord.Address, record.Port))); + foreach (var address in record.Addresses) + { + ttl = MinTtl(now, address.ExpiresAt, ttl); + endpoints.Add(CreateEndpoint(new IPEndPoint(address.Address, record.Port))); + } } - else if (targetRecord is CNameRecord canonicalNameRecord) + else { - endpoints.Add(CreateEndpoint(new DnsEndPoint(canonicalNameRecord.CanonicalName.Value.TrimEnd('.'), record.Port))); + endpoints.Add(CreateEndpoint(new DnsEndPoint(record.Target.TrimEnd('.'), record.Port))); } } SetResult(endpoints, ttl); - static TimeSpan MinTtl(DnsResourceRecord record, TimeSpan existing) + static TimeSpan MinTtl(DateTime now, DateTime expiresAt, TimeSpan existing) { - var candidate = TimeSpan.FromSeconds(record.TimeToLive); + var candidate = expiresAt - now; return candidate < existing ? candidate : existing; } - InvalidOperationException CreateException(string dnsName, string errorMessage) - { - var msg = errorMessage switch - { - { Length: > 0 } => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}'): {errorMessage}.", - _ => $"No DNS records were found for service '{ServiceName}' (DNS name: '{dnsName}')." - }; - return new InvalidOperationException(msg); - } - ServiceEndpoint CreateEndpoint(EndPoint endPoint) { var serviceEndpoint = ServiceEndpoint.Create(endPoint); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index fd0cb28353d..085ee30123b 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -2,29 +2,29 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using DnsClient; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.ServiceDiscovery.Dns; internal sealed partial class DnsSrvServiceEndpointProviderFactory( IOptionsMonitor options, ILogger logger, - IDnsQuery dnsClient, + IDnsResolver resolver, 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"); private static readonly string s_resolveConfPath = Path.Combine($"{Path.DirectorySeparatorChar}etc", "resolv.conf"); - private readonly string? _querySuffix = options.CurrentValue.QuerySuffix ?? GetKubernetesHostDomain(); + private readonly string? _querySuffix = options.CurrentValue.QuerySuffix?.TrimStart('.') ?? GetKubernetesHostDomain(); /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { // 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 - // SRV records are available for headless services with named ports. + // SRV records are available for headless services with named ports. // They take the form $"_{portName}._{protocol}.{serviceName}.{namespace}.{suffix}" // The suffix (after the service name) can be parsed from /etc/resolv.conf // Otherwise, the namespace can be read from /var/run/secrets/kubernetes.io/serviceaccount/namespace and combined with an assumed suffix of "svc.cluster.local". @@ -39,7 +39,7 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou var portName = query.EndpointName ?? "default"; var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; - provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, dnsClient, timeProvider); + provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 9854c93c01a..3aba9b3aaea 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -9,7 +9,6 @@ - @@ -23,7 +22,9 @@ - + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs new file mode 100644 index 00000000000..094df3040d1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataReader.cs @@ -0,0 +1,133 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsDataReader : IDisposable +{ + public ArraySegment MessageBuffer { get; private set; } + bool _returnToPool; + private int _position; + + public DnsDataReader(ArraySegment buffer, bool returnToPool = false) + { + MessageBuffer = buffer; + _position = 0; + _returnToPool = returnToPool; + } + + public bool TryReadHeader(out DnsMessageHeader header) + { + Debug.Assert(_position == 0); + + if (!DnsPrimitives.TryReadMessageHeader(MessageBuffer.AsSpan(), out header, out int bytesRead)) + { + header = default; + return false; + } + + _position += bytesRead; + return true; + } + + internal bool TryReadQuestion(out EncodedDomainName name, out QueryType type, out QueryClass @class) + { + if (!TryReadDomainName(out name) || + !TryReadUInt16(out ushort typeAsInt) || + !TryReadUInt16(out ushort classAsInt)) + { + type = 0; + @class = 0; + return false; + } + + type = (QueryType)typeAsInt; + @class = (QueryClass)classAsInt; + return true; + } + + public bool TryReadUInt16(out ushort value) + { + if (MessageBuffer.Count - _position < 2) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt16BigEndian(MessageBuffer.AsSpan(_position)); + _position += 2; + return true; + } + + public bool TryReadUInt32(out uint value) + { + if (MessageBuffer.Count - _position < 4) + { + value = 0; + return false; + } + + value = BinaryPrimitives.ReadUInt32BigEndian(MessageBuffer.AsSpan(_position)); + _position += 4; + return true; + } + + public bool TryReadResourceRecord(out DnsResourceRecord record) + { + if (!TryReadDomainName(out EncodedDomainName name) || + !TryReadUInt16(out ushort type) || + !TryReadUInt16(out ushort @class) || + !TryReadUInt32(out uint ttl) || + !TryReadUInt16(out ushort dataLength) || + MessageBuffer.Count - _position < dataLength) + { + record = default; + return false; + } + + ReadOnlyMemory data = MessageBuffer.AsMemory(_position, dataLength); + _position += dataLength; + + record = new DnsResourceRecord(name, (QueryType)type, (QueryClass)@class, (int)ttl, data); + return true; + } + + public bool TryReadDomainName(out EncodedDomainName name) + { + if (DnsPrimitives.TryReadQName(MessageBuffer, _position, out name, out int bytesRead)) + { + _position += bytesRead; + return true; + } + + return false; + } + + public bool TryReadSpan(int length, out ReadOnlySpan name) + { + if (MessageBuffer.Count - _position < length) + { + name = default; + return false; + } + + name = MessageBuffer.AsSpan(_position, length); + _position += length; + return true; + } + + public void Dispose() + { + if (_returnToPool && MessageBuffer.Array != null) + { + ArrayPool.Shared.Return(MessageBuffer.Array); + } + + _returnToPool = false; + MessageBuffer = default; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs new file mode 100644 index 00000000000..a0a11f0b808 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsDataWriter.cs @@ -0,0 +1,121 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Binary; +using System.Diagnostics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed class DnsDataWriter +{ + private readonly Memory _buffer; + private int _position; + + internal DnsDataWriter(Memory buffer) + { + _buffer = buffer; + _position = 0; + } + + public int Position => _position; + + internal bool TryWriteHeader(in DnsMessageHeader header) + { + if (!DnsPrimitives.TryWriteMessageHeader(_buffer.Span.Slice(_position), header, out int written)) + { + return false; + } + + _position += written; + return true; + } + + internal bool TryWriteQuestion(EncodedDomainName name, QueryType type, QueryClass @class) + { + if (!TryWriteDomainName(name) || + !TryWriteUInt16((ushort)type) || + !TryWriteUInt16((ushort)@class)) + { + return false; + } + + return true; + } + + private bool TryWriteDomainName(EncodedDomainName name) + { + foreach (var label in name.Labels) + { + // this should be already validated by the caller + Debug.Assert(label.Length <= 63, "Label length must not exceed 63 bytes."); + + if (!TryWriteByte((byte)label.Length) || + !TryWriteRawData(label.Span)) + { + return false; + } + } + + // root label + return TryWriteByte(0); + } + + internal bool TryWriteDomainName(string name) + { + if (DnsPrimitives.TryWriteQName(_buffer.Span.Slice(_position), name, out int written)) + { + _position += written; + return true; + } + + return false; + } + + internal bool TryWriteByte(byte value) + { + if (_buffer.Length - _position < 1) + { + return false; + } + + _buffer.Span[_position] = value; + _position += 1; + return true; + } + + internal bool TryWriteUInt16(ushort value) + { + if (_buffer.Length - _position < 2) + { + return false; + } + + BinaryPrimitives.WriteUInt16BigEndian(_buffer.Span.Slice(_position), value); + _position += 2; + return true; + } + + internal bool TryWriteUInt32(uint value) + { + if (_buffer.Length - _position < 4) + { + return false; + } + + BinaryPrimitives.WriteUInt32BigEndian(_buffer.Span.Slice(_position), value); + _position += 4; + return true; + } + + internal bool TryWriteRawData(ReadOnlySpan value) + { + if (_buffer.Length - _position < value.Length) + { + return false; + } + + value.CopyTo(_buffer.Span.Slice(_position)); + _position += value.Length; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs new file mode 100644 index 00000000000..b22273a04f2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsMessageHeader.cs @@ -0,0 +1,36 @@ +// 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.Dns.Resolver; + +// RFC 1035 4.1.1. Header section format +internal struct DnsMessageHeader +{ + internal const int HeaderLength = 12; + public ushort TransactionId { get; set; } + + internal QueryFlags QueryFlags { get; set; } + + public ushort QueryCount { get; set; } + + public ushort AnswerCount { get; set; } + + public ushort AuthorityCount { get; set; } + + public ushort AdditionalRecordCount { get; set; } + + public QueryResponseCode ResponseCode + { + get => (QueryResponseCode)(QueryFlags & QueryFlags.ResponseCodeMask); + } + + public bool IsResultTruncated + { + get => (QueryFlags & QueryFlags.ResultTruncated) != 0; + } + + public bool IsResponse + { + get => (QueryFlags & QueryFlags.HasResponse) != 0; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs new file mode 100644 index 00000000000..e549abe2576 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsPrimitives.cs @@ -0,0 +1,318 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Globalization; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class DnsPrimitives +{ + // Maximum length of a domain name in ASCII (excluding trailing dot) + internal const int MaxDomainNameLength = 253; + + internal static bool TryReadMessageHeader(ReadOnlySpan buffer, out DnsMessageHeader header, out int bytesRead) + { + // RFC 1035 4.1.1. Header section format + if (buffer.Length < DnsMessageHeader.HeaderLength) + { + header = default; + bytesRead = 0; + return false; + } + + header = new DnsMessageHeader + { + TransactionId = BinaryPrimitives.ReadUInt16BigEndian(buffer), + QueryFlags = (QueryFlags)BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(2)), + QueryCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(4)), + AnswerCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(6)), + AuthorityCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(8)), + AdditionalRecordCount = BinaryPrimitives.ReadUInt16BigEndian(buffer.Slice(10)) + }; + + bytesRead = DnsMessageHeader.HeaderLength; + return true; + } + + internal static bool TryWriteMessageHeader(Span buffer, DnsMessageHeader header, out int bytesWritten) + { + // RFC 1035 4.1.1. Header section format + if (buffer.Length < DnsMessageHeader.HeaderLength) + { + bytesWritten = 0; + return false; + } + + BinaryPrimitives.WriteUInt16BigEndian(buffer, header.TransactionId); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(2), (ushort)header.QueryFlags); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(4), header.QueryCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(6), header.AnswerCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(8), header.AuthorityCount); + BinaryPrimitives.WriteUInt16BigEndian(buffer.Slice(10), header.AdditionalRecordCount); + + bytesWritten = DnsMessageHeader.HeaderLength; + return true; + } + + // https://www.rfc-editor.org/rfc/rfc1035#section-2.3.4 + // labels 63 octets or less + // name 255 octets or less + + private static readonly SearchValues s_domainNameValidChars = SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."); + private static readonly IdnMapping s_idnMapping = new IdnMapping(); + internal static bool TryWriteQName(Span destination, string name, out int written) + { + written = 0; + + // + // RFC 1035 4.1.2. + // + // a domain name represented as a sequence of labels, where + // each label consists of a length octet followed by that + // number of octets. The domain name terminates with the + // zero length octet for the null label of the root. Note + // that this field may be an odd number of octets; no + // padding is used. + // + if (!Ascii.IsValid(name)) + { + // IDN name, apply punycode + try + { + // IdnMapping performs some validation internally (such as label + // and domain name lengths), but is more relaxed than RFC + // 1035 (e.g. allows ~ chars), so even if this conversion does + // not throw, we still need to perform additional validation + name = s_idnMapping.GetAscii(name); + } + catch + { + return false; + } + } + + if (name.Length > MaxDomainNameLength || + name.AsSpan().ContainsAnyExcept(s_domainNameValidChars) || + destination.IsEmpty || + !Encoding.ASCII.TryGetBytes(name, destination.Slice(1), out int length) || + destination.Length < length + 2) + { + // buffer too small + return false; + } + + Span nameBuffer = destination.Slice(0, 1 + length); + Span label; + while (true) + { + // figure out the next label and prepend the length + int index = nameBuffer.Slice(1).IndexOf((byte)'.'); + label = index == -1 ? nameBuffer.Slice(1) : nameBuffer.Slice(1, index); + + if (label.Length == 0) + { + // empty label (explicit root) is only allowed at the end + if (index != -1) + { + written = 0; + return false; + } + } + // Label restrictions: + // - maximum 63 octets long + // - must start with a letter or digit (digit is allowed by RFC 1123) + // - may start with an underscore (underscore may be present only + // at the start of the label to support SRV records) + // - must end with a letter or digit + else if (label.Length > 63 || + !char.IsAsciiLetterOrDigit((char)label[0]) && label[0] != '_' || + label.Slice(1).Contains((byte)'_') || + !char.IsAsciiLetterOrDigit((char)label[^1])) + { + written = 0; + return false; + } + + nameBuffer[0] = (byte)label.Length; + written += label.Length + 1; + + if (index == -1) + { + // this was the last label + break; + } + + nameBuffer = nameBuffer.Slice(index + 1); + } + + // Add root label if wasn't explicitly specified + if (label.Length != 0) + { + destination[written] = 0; + written++; + } + + return true; + } + + private static bool TryReadQNameCore(List> labels, int totalLength, ReadOnlyMemory messageBuffer, int offset, out int bytesRead, bool canStartWithPointer = true) + { + // + // domain name can be either + // - a sequence of labels, where each label consists of a length octet + // followed by that number of octets, terminated by a zero length octet + // (root label) + // - a pointer, where the first two bits are set to 1, and the remaining + // 14 bits are an offset (from the start of the message) to the true + // label + // + // It is not specified by the RFC if pointers must be backwards only, + // the code below prohibits forward (and self) pointers to avoid + // infinite loops. It also allows pointers only to point to a + // label, not to another pointer. + // + + bytesRead = 0; + bool allowPointer = canStartWithPointer; + + if (offset < 0 || offset >= messageBuffer.Length) + { + return false; + } + + int currentOffset = offset; + + while (true) + { + byte length = messageBuffer.Span[currentOffset]; + + if ((length & 0xC0) == 0x00) + { + // length followed by the label + if (length == 0) + { + // end of name + bytesRead = currentOffset - offset + 1; + return true; + } + + if (currentOffset + 1 + length >= messageBuffer.Length) + { + // too many labels or truncated data + break; + } + + // read next label/segment + labels.Add(messageBuffer.Slice(currentOffset + 1, length)); + totalLength += 1 + length; + + // subtract one for the length prefix of the first label + if (totalLength - 1 > MaxDomainNameLength) + { + // domain name is too long + return false; + } + + currentOffset += 1 + length; + bytesRead += 1 + length; + + // we read a label, they can be followed by pointer. + allowPointer = true; + } + else if ((length & 0xC0) == 0xC0) + { + // pointer, together with next byte gives the offset of the true label + if (!allowPointer || currentOffset + 1 >= messageBuffer.Length) + { + // pointer to pointer or truncated data + break; + } + + bytesRead += 2; + int pointer = ((length & 0x3F) << 8) | messageBuffer.Span[currentOffset + 1]; + + // we prohibit self-references and forward pointers to avoid + // infinite loops, we do this by truncating the + // messageBuffer at the offset where we started reading the + // name. We also ignore the bytesRead from the recursive + // call, as we are only interested on how many bytes we read + // from the initial start of the name. + return TryReadQNameCore(labels, totalLength, messageBuffer.Slice(0, offset), pointer, out int _, false); + } + else + { + // top two bits are reserved, this means invalid data + break; + } + } + + return false; + + } + + internal static bool TryReadQName(ReadOnlyMemory messageBuffer, int offset, out EncodedDomainName name, out int bytesRead) + { + List> labels = new List>(); + + if (TryReadQNameCore(labels, 0, messageBuffer, offset, out bytesRead)) + { + name = new EncodedDomainName(labels); + return true; + } + else + { + bytesRead = 0; + name = default; + return false; + } + } + + internal static bool TryReadService(ReadOnlyMemory buffer, out ushort priority, out ushort weight, out ushort port, out EncodedDomainName target, out int bytesRead) + { + // https://www.rfc-editor.org/rfc/rfc2782 + if (!BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span, out priority) || + !BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span.Slice(2), out weight) || + !BinaryPrimitives.TryReadUInt16BigEndian(buffer.Span.Slice(4), out port) || + !TryReadQName(buffer.Slice(6), 0, out target, out bytesRead)) + { + target = default; + priority = 0; + weight = 0; + port = 0; + bytesRead = 0; + return false; + } + + bytesRead += 6; + return true; + } + + internal static bool TryReadSoa(ReadOnlyMemory buffer, out EncodedDomainName primaryNameServer, out EncodedDomainName responsibleMailAddress, out uint serial, out uint refresh, out uint retry, out uint expire, out uint minimum, out int bytesRead) + { + // https://www.rfc-editor.org/rfc/rfc1035#section-3.3.13 + if (!TryReadQName(buffer, 0, out primaryNameServer, out int w1) || + !TryReadQName(buffer.Slice(w1), 0, out responsibleMailAddress, out int w2) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2), out serial) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 4), out refresh) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 8), out retry) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 12), out expire) || + !BinaryPrimitives.TryReadUInt32BigEndian(buffer.Span.Slice(w1 + w2 + 16), out minimum)) + { + primaryNameServer = default; + responsibleMailAddress = default; + serial = 0; + refresh = 0; + retry = 0; + expire = 0; + minimum = 0; + bytesRead = 0; + return false; + } + + bytesRead = w1 + w2 + 20; + return true; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs new file mode 100644 index 00000000000..adab9161737 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Log.cs @@ -0,0 +1,39 @@ +// 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; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal partial class DnsResolver : IDnsResolver, IDisposable +{ + internal static partial class Log + { + [LoggerMessage(1, LogLevel.Debug, "Resolving {QueryType} {QueryName} on {Server} attempt {Attempt}", EventName = "Query")] + public static partial void Query(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(2, LogLevel.Debug, "Result truncated for {QueryType} {QueryName} from {Server} attempt {Attempt}. Restarting over TCP", EventName = "ResultTruncated")] + public static partial void ResultTruncated(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(3, LogLevel.Error, "Server {Server} replied with {ResponseCode} when querying {QueryType} {QueryName}", EventName = "ErrorResponseCode")] + public static partial void ErrorResponseCode(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, QueryResponseCode responseCode); + + [LoggerMessage(4, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} timed out.", EventName = "Timeout")] + public static partial void Timeout(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(5, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt}: no data matching given query type.", EventName = "NoData")] + public static partial void NoData(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(6, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt}: server indicates given name does not exist.", EventName = "NameError")] + public static partial void NameError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(7, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed to return a valid DNS response.", EventName = "MalformedResponse")] + public static partial void MalformedResponse(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt); + + [LoggerMessage(8, LogLevel.Warning, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed due to a network error.", EventName = "NetworkError")] + public static partial void NetworkError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt, Exception exception); + + [LoggerMessage(9, LogLevel.Error, "Query {QueryType} {QueryName} on {Server} attempt {Attempt} failed.", EventName = "QueryError")] + public static partial void QueryError(ILogger logger, QueryType queryType, string queryName, IPEndPoint server, int attempt, Exception exception); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs new file mode 100644 index 00000000000..4be956cede9 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.Telemetry.cs @@ -0,0 +1,115 @@ +// 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.Diagnostics.Metrics; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal partial class DnsResolver +{ + internal static class Telemetry + { + private static readonly Meter s_meter = new Meter("Microsoft.Extensions.ServiceDiscovery.Dns.Resolver"); + private static readonly Histogram s_queryDuration = s_meter.CreateHistogram("query.duration", "ms", "DNS query duration"); + + private static bool IsEnabled() => s_queryDuration.Enabled; + + public static NameResolutionActivity StartNameResolution(string hostName, QueryType queryType, long startingTimestamp) + { + if (IsEnabled()) + { + return new NameResolutionActivity(hostName, queryType, startingTimestamp); + } + + return default; + } + + public static void StopNameResolution(string hostName, QueryType queryType, in NameResolutionActivity activity, object? answers, SendQueryError error, long endingTimestamp) + { + activity.Stop(answers, error, endingTimestamp, out TimeSpan duration); + + if (!IsEnabled()) + { + return; + } + + var hostNameTag = KeyValuePair.Create("dns.question.name", (object?)hostName); + var queryTypeTag = KeyValuePair.Create("dns.question.type", (object?)queryType); + + if (answers is not null) + { + s_queryDuration.Record(duration.TotalSeconds, hostNameTag, queryTypeTag); + } + else + { + var errorTypeTag = KeyValuePair.Create("error.type", (object?)error.ToString()); + s_queryDuration.Record(duration.TotalSeconds, hostNameTag, queryTypeTag, errorTypeTag); + } + } + } + + internal readonly struct NameResolutionActivity + { + private const string ActivitySourceName = "Microsoft.Extensions.ServiceDiscovery.Dns.Resolver"; + private const string ActivityName = ActivitySourceName + ".Resolve"; + private static readonly ActivitySource s_activitySource = new ActivitySource(ActivitySourceName); + + private readonly long _startingTimestamp; + private readonly Activity? _activity; // null if activity is not started + + public NameResolutionActivity(string hostName, QueryType queryType, long startingTimestamp) + { + _startingTimestamp = startingTimestamp; + _activity = s_activitySource.StartActivity(ActivityName, ActivityKind.Client); + if (_activity is not null) + { + _activity.DisplayName = $"Resolving {hostName}"; + if (_activity.IsAllDataRequested) + { + _activity.SetTag("dns.question.name", hostName); + _activity.SetTag("dns.question.type", queryType.ToString()); + } + } + } + + public void Stop(object? answers, SendQueryError error, long endingTimestamp, out TimeSpan duration) + { + duration = Stopwatch.GetElapsedTime(_startingTimestamp, endingTimestamp); + + if (_activity is null) + { + return; + } + + if (_activity.IsAllDataRequested) + { + if (answers is not null) + { + static string[] ToStringHelper(T[] array) => array.Select(a => a!.ToString()!).ToArray(); + + string[]? answersArray = answers switch + { + ServiceResult[] serviceResults => ToStringHelper(serviceResults), + AddressResult[] addressResults => ToStringHelper(addressResults), + _ => null + }; + + Debug.Assert(answersArray is not null); + _activity.SetTag("dns.answers", answersArray); + } + else + { + _activity.SetTag("error.type", error.ToString()); + } + } + + if (answers is null) + { + _activity.SetStatus(ActivityStatusCode.Error); + } + + _activity.Stop(); + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs new file mode 100644 index 00000000000..5722356a1c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -0,0 +1,931 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed partial class DnsResolver : IDnsResolver, IDisposable +{ + private const int IPv4Length = 4; + private const int IPv6Length = 16; + + // CancellationTokenSource.CancelAfter has a maximum timeout of Int32.MaxValue milliseconds. + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + + private bool _disposed; + private readonly ResolverOptions _options; + private readonly CancellationTokenSource _pendingRequestsCts = new(); + private readonly TimeProvider _timeProvider; + private readonly ILogger _logger; + + public DnsResolver(TimeProvider timeProvider, ILogger logger) : this(timeProvider, logger, OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() ? ResolvConf.GetOptions() : NetworkInfo.GetOptions()) + { + } + + internal DnsResolver(TimeProvider timeProvider, ILogger logger, ResolverOptions options) + { + _timeProvider = timeProvider; + _logger = logger; + _options = options; + Debug.Assert(_options.Servers.Count > 0); + + if (options.Timeout != Timeout.InfiniteTimeSpan) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); + ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Timeout, s_maxTimeout); + } + } + + internal DnsResolver(ResolverOptions options) : this(TimeProvider.System, NullLogger.Instance, options) + { + } + + internal DnsResolver(IEnumerable servers) : this(new ResolverOptions(servers.ToArray())) + { + } + + internal DnsResolver(IPEndPoint server) : this(new ResolverOptions(server)) + { + } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); + + // dnsSafeName is Disposed by SendQueryWithTelemetry + EncodedDomainName dnsSafeName = GetNormalizedHostName(name); + return SendQueryWithTelemetry(name, dnsSafeName, QueryType.SRV, ProcessResponse, cancellationToken); + + static (SendQueryError, ServiceResult[]) ProcessResponse(EncodedDomainName dnsSafeName, QueryType queryType, DnsResponse response) + { + var results = new List(response.Answers.Count); + + foreach (var answer in response.Answers) + { + if (answer.Type == QueryType.SRV) + { + if (!DnsPrimitives.TryReadService(answer.Data, out ushort priority, out ushort weight, out ushort port, out EncodedDomainName target, out int bytesRead) || bytesRead != answer.Data.Length) + { + return (SendQueryError.MalformedResponse, []); + } + + List addresses = new List(); + foreach (var additional in response.Additionals) + { + // From RFC 2782: + // + // Target + // The domain name of the target host. There MUST be one or more + // address records for this name, the name MUST NOT be an alias (in + // the sense of RFC 1034 or RFC 2181). Implementors are urged, but + // not required, to return the address record(s) in the Additional + // Data section. Unless and until permitted by future standards + // action, name compression is not to be used for this field. + // + // A Target of "." means that the service is decidedly not + // available at this domain. + if (additional.Name.Equals(target) && (additional.Type == QueryType.A || additional.Type == QueryType.AAAA)) + { + addresses.Add(new AddressResult(response.CreatedAt.AddSeconds(additional.Ttl), new IPAddress(additional.Data.Span))); + } + } + + results.Add(new ServiceResult(response.CreatedAt.AddSeconds(answer.Ttl), priority, weight, port, target.ToString(), addresses.ToArray())); + } + } + + return (SendQueryError.NoError, results.ToArray()); + } + } + + public async ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) + { + if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // name localhost exists outside of DNS and can't be resolved by a DNS server + int len = (Socket.OSSupportsIPv4 ? 1 : 0) + (Socket.OSSupportsIPv6 ? 1 : 0); + AddressResult[] res = new AddressResult[len]; + + int index = 0; + if (Socket.OSSupportsIPv6) // prefer IPv6 + { + res[index] = new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback); + index++; + } + if (Socket.OSSupportsIPv4) + { + res[index] = new AddressResult(DateTime.MaxValue, IPAddress.Loopback); + } + + return res; + } + + var ipv4AddressesTask = ResolveIPAddressesAsync(name, AddressFamily.InterNetwork, cancellationToken); + var ipv6AddressesTask = ResolveIPAddressesAsync(name, AddressFamily.InterNetworkV6, cancellationToken); + + AddressResult[] ipv4Addresses = await ipv4AddressesTask.ConfigureAwait(false); + AddressResult[] ipv6Addresses = await ipv6AddressesTask.ConfigureAwait(false); + + AddressResult[] results = new AddressResult[ipv4Addresses.Length + ipv6Addresses.Length]; + ipv6Addresses.CopyTo(results, 0); + ipv4Addresses.CopyTo(results, ipv6Addresses.Length); + return results; + } + + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(_disposed, this); + cancellationToken.ThrowIfCancellationRequested(); + + if (addressFamily != AddressFamily.InterNetwork && addressFamily != AddressFamily.InterNetworkV6) + { + throw new ArgumentOutOfRangeException(nameof(addressFamily), addressFamily, "Invalid address family"); + } + + if (string.Equals(name, "localhost", StringComparison.OrdinalIgnoreCase)) + { + // name localhost exists outside of DNS and can't be resolved by a DNS server + if (addressFamily == AddressFamily.InterNetwork && Socket.OSSupportsIPv4) + { + return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.Loopback)]); + } + else if (addressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6) + { + return ValueTask.FromResult([new AddressResult(DateTime.MaxValue, IPAddress.IPv6Loopback)]); + } + + return ValueTask.FromResult([]); + } + + // dnsSafeName is Disposed by SendQueryWithTelemetry + EncodedDomainName dnsSafeName = GetNormalizedHostName(name); + var queryType = addressFamily == AddressFamily.InterNetwork ? QueryType.A : QueryType.AAAA; + return SendQueryWithTelemetry(name, dnsSafeName, queryType, ProcessResponse, cancellationToken); + + static (SendQueryError error, AddressResult[] result) ProcessResponse(EncodedDomainName dnsSafeName, QueryType queryType, DnsResponse response) + { + List results = new List(response.Answers.Count); + + // Servers send back CNAME records together with associated A/AAAA records. Servers + // send only those CNAME records relevant to the query, and if there is a CNAME record, + // there should not be other records associated with the name. Therefore, we simply follow + // the list of CNAME aliases until we get to the primary name and return the A/AAAA records + // associated. + // + // more info: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 + // + // Most of the servers send the CNAME records in order so that we can sequentially scan the + // answers, but nothing prevents the records from being in arbitrary order. Attempt the linear + // scan first and fallback to a slower but more robust method if necessary. + + bool success = true; + EncodedDomainName currentAlias = dnsSafeName; + + foreach (var answer in response.Answers) + { + switch (answer.Type) + { + case QueryType.CNAME: + if (!TryReadTarget(answer, response.RawMessageBytes, out EncodedDomainName target)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (answer.Name.Equals(currentAlias)) + { + currentAlias = target; + continue; + } + + break; + + case var type when type == queryType: + if (!TryReadAddress(answer, queryType, out IPAddress? address)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (answer.Name.Equals(currentAlias)) + { + results.Add(new AddressResult(response.CreatedAt.AddSeconds(answer.Ttl), address)); + continue; + } + + break; + } + + // unexpected name or record type, fall back to more robust path + results.Clear(); + success = false; + break; + } + + if (success) + { + return (SendQueryError.NoError, results.ToArray()); + } + + // more expensive path for uncommon (but valid) cases where CNAME records are out of order. Use of Dictionary + // allows us to stay within O(n) complexity for the number of answers, but we will use more memory. + Dictionary aliasMap = new(); + Dictionary> aRecordMap = new(); + foreach (var answer in response.Answers) + { + if (answer.Type == QueryType.CNAME) + { + // map the alias to the target name + if (!TryReadTarget(answer, response.RawMessageBytes, out EncodedDomainName target)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (!aliasMap.TryAdd(answer.Name, target)) + { + // Duplicate CNAME record + return (SendQueryError.MalformedResponse, []); + } + } + + if (answer.Type == queryType) + { + if (!TryReadAddress(answer, queryType, out IPAddress? address)) + { + return (SendQueryError.MalformedResponse, []); + } + + if (!aRecordMap.TryGetValue(answer.Name, out List? addressList)) + { + addressList = new List(); + aRecordMap.Add(answer.Name, addressList); + } + + addressList.Add(new AddressResult(response.CreatedAt.AddSeconds(answer.Ttl), address)); + } + } + + // follow the CNAME chain, limit the maximum number of iterations to avoid infinite loops. + int i = 0; + currentAlias = dnsSafeName; + while (aliasMap.TryGetValue(currentAlias, out EncodedDomainName nextAlias)) + { + if (i >= aliasMap.Count) + { + // circular CNAME chain + return (SendQueryError.MalformedResponse, []); + } + + i++; + + if (aRecordMap.ContainsKey(currentAlias)) + { + // both CNAME record and A/AAAA records exist for the current alias + return (SendQueryError.MalformedResponse, []); + } + + currentAlias = nextAlias; + } + + // Now we have the final target name, check if we have any A/AAAA records for it. + aRecordMap.TryGetValue(currentAlias, out List? finalAddressList); + return (SendQueryError.NoError, finalAddressList?.ToArray() ?? []); + + static bool TryReadTarget(in DnsResourceRecord record, ArraySegment messageBytes, out EncodedDomainName target) + { + Debug.Assert(record.Type == QueryType.CNAME, "Only CNAME records should be processed here."); + + target = default; + + // some servers use domain name compression even inside CNAME records. In order to decode those + // correctly, we need to pass the entire message to TryReadQName. The Data span inside the record + // should be backed by the array containing the entire DNS message. We just need to account for the + // 2 byte offset in case of TCP fallback. + var gotArray = MemoryMarshal.TryGetArray(record.Data, out ArraySegment segment); + Debug.Assert(gotArray, "Failed to get array segment"); + Debug.Assert(segment.Array == messageBytes.Array, "record data backed by different array than the original message"); + + int messageOffset = messageBytes.Offset; + + bool result = DnsPrimitives.TryReadQName(segment.Array.AsMemory(messageOffset, segment.Offset + segment.Count - messageOffset), segment.Offset - messageOffset, out EncodedDomainName targetName, out int bytesRead) && bytesRead == record.Data.Length; + if (result) + { + target = targetName; + } + + return result; + } + + static bool TryReadAddress(in DnsResourceRecord record, QueryType type, [NotNullWhen(true)] out IPAddress? target) + { + Debug.Assert(record.Type is QueryType.A or QueryType.AAAA, "Only CNAME records should be processed here."); + + target = null; + if (record.Type == QueryType.A && record.Data.Length != IPv4Length || + record.Type == QueryType.AAAA && record.Data.Length != IPv6Length) + { + return false; + } + + target = new IPAddress(record.Data.Span); + return true; + } + } + } + + private async ValueTask SendQueryWithTelemetry(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func processResponseFunc, CancellationToken cancellationToken) + { + NameResolutionActivity activity = Telemetry.StartNameResolution(name, queryType, _timeProvider.GetTimestamp()); + (SendQueryError error, TResult[] result) = await SendQueryWithRetriesAsync(name, dnsSafeName, queryType, processResponseFunc, cancellationToken).ConfigureAwait(false); + Telemetry.StopNameResolution(name, queryType, activity, null, error, _timeProvider.GetTimestamp()); + dnsSafeName.Dispose(); + + return result; + } + + internal struct SendQueryResult + { + public DnsResponse Response; + public SendQueryError Error; + } + + async ValueTask<(SendQueryError error, TResult[] result)> SendQueryWithRetriesAsync(string name, EncodedDomainName dnsSafeName, QueryType queryType, Func processResponseFunc, CancellationToken cancellationToken) + { + SendQueryError lastError = SendQueryError.InternalError; // will be overwritten by the first attempt + for (int index = 0; index < _options.Servers.Count; index++) + { + IPEndPoint serverEndPoint = _options.Servers[index]; + + for (int attempt = 1; attempt <= _options.Attempts; attempt++) + { + DnsResponse response = default; + try + { + TResult[] results = Array.Empty(); + + try + { + SendQueryResult queryResult = await SendQueryToServerWithTimeoutAsync(serverEndPoint, name, dnsSafeName, queryType, attempt, cancellationToken).ConfigureAwait(false); + lastError = queryResult.Error; + response = queryResult.Response; + + if (lastError == SendQueryError.NoError) + { + // Given that result.Error is NoError, there should be at least one answer. + Debug.Assert(response.Answers.Count > 0); + (lastError, results) = processResponseFunc(dnsSafeName, queryType, queryResult.Response); + } + } + catch (SocketException ex) + { + Log.NetworkError(_logger, queryType, name, serverEndPoint, attempt, ex); + lastError = SendQueryError.NetworkError; + } + catch (Exception ex) when (!cancellationToken.IsCancellationRequested) + { + // internal error, propagate + Log.QueryError(_logger, queryType, name, serverEndPoint, attempt, ex); + throw; + } + + switch (lastError) + { + // + // Definitive answers, no point retrying + // + case SendQueryError.NoError: + return (lastError, results); + + case SendQueryError.NameError: + // authoritative answer that the name does not exist, no point in retrying + Log.NameError(_logger, queryType, name, serverEndPoint, attempt); + return (lastError, results); + + case SendQueryError.NoData: + // no data available for the name from authoritative server + Log.NoData(_logger, queryType, name, serverEndPoint, attempt); + return (lastError, results); + + // + // Transient errors, retry on the same server + // + case SendQueryError.Timeout: + Log.Timeout(_logger, queryType, name, serverEndPoint, attempt); + continue; + + case SendQueryError.NetworkError: + // TODO: retry with exponential backoff? + continue; + + case SendQueryError.ServerError when response.Header.ResponseCode == QueryResponseCode.ServerFailure: + // ServerFailure may indicate transient failure with upstream DNS servers, retry on the same server + Log.ErrorResponseCode(_logger, queryType, name, serverEndPoint, response.Header.ResponseCode); + continue; + + // + // Persistent errors, skip to the next server + // + case SendQueryError.ServerError: + // this should cover all response codes except NoError, NameError which are definite and handled above, and + // ServerFailure which is a transient error and handled above. + Log.ErrorResponseCode(_logger, queryType, name, serverEndPoint, response.Header.ResponseCode); + break; + + case SendQueryError.MalformedResponse: + Log.MalformedResponse(_logger, queryType, name, serverEndPoint, attempt); + break; + + case SendQueryError.InternalError: + // exception logged above. + break; + } + + // actual break that causes skipping to the next server + break; + } + finally + { + response.Dispose(); + } + } + } + + // if we get here, we exhausted all servers and all attempts + return (lastError, []); + } + + internal async ValueTask SendQueryToServerWithTimeoutAsync(IPEndPoint serverEndPoint, string name, EncodedDomainName dnsSafeName, QueryType queryType, int attempt, CancellationToken cancellationToken) + { + (CancellationTokenSource cts, bool disposeTokenSource, CancellationTokenSource pendingRequestsCts) = PrepareCancellationTokenSource(cancellationToken); + + try + { + return await SendQueryToServerAsync(serverEndPoint, name, dnsSafeName, queryType, attempt, cts.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when ( + !cancellationToken.IsCancellationRequested && // not cancelled by the caller + !pendingRequestsCts.IsCancellationRequested) // not cancelled by the global token (dispose) + // the only remaining token that could cancel this is the linked cts from the timeout. + { + Debug.Assert(cts.Token.IsCancellationRequested); + return new SendQueryResult { Error = SendQueryError.Timeout }; + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested && ex.CancellationToken != cancellationToken) + { + // cancellation was initiated by the caller, but exception was triggered by a linked token, + // rethrow the exception with the caller's token. + cancellationToken.ThrowIfCancellationRequested(); + throw new UnreachableException(); + } + finally + { + if (disposeTokenSource) + { + cts.Dispose(); + } + } + } + + private async ValueTask SendQueryToServerAsync(IPEndPoint serverEndPoint, string name, EncodedDomainName dnsSafeName, QueryType queryType, int attempt, CancellationToken cancellationToken) + { + Log.Query(_logger, queryType, name, serverEndPoint, attempt); + + SendQueryError sendError = SendQueryError.NoError; + DateTime queryStartedTime = _timeProvider.GetUtcNow().DateTime; + DnsDataReader responseReader = default; + DnsMessageHeader header; + + try + { + // use transport override if provided + if (_options._transportOverride != null) + { + (responseReader, header, sendError) = SendDnsQueryCustomTransport(_options._transportOverride, dnsSafeName, queryType); + } + else + { + (responseReader, header) = await SendDnsQueryCoreUdpAsync(serverEndPoint, dnsSafeName, queryType, cancellationToken).ConfigureAwait(false); + + if (header.IsResultTruncated) + { + Log.ResultTruncated(_logger, queryType, name, serverEndPoint, 0); + responseReader.Dispose(); + // TCP fallback + (responseReader, header, sendError) = await SendDnsQueryCoreTcpAsync(serverEndPoint, dnsSafeName, queryType, cancellationToken).ConfigureAwait(false); + } + } + + if (sendError != SendQueryError.NoError) + { + // we failed to get back any response + return new SendQueryResult { Error = sendError }; + } + + if ((uint)header.ResponseCode > (uint)QueryResponseCode.Refused) + { + // Response code is outside of valid range + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + // Recheck that the server echoes back the DNS question + if (header.QueryCount != 1 || + !responseReader.TryReadQuestion(out var qName, out var qType, out var qClass) || + !dnsSafeName.Equals(qName) || qType != queryType || qClass != QueryClass.Internet) + { + // DNS Question mismatch + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + // Structurally separate the resource records, this will validate only the + // "outside structure" of the resource record, it will not validate the content. + int ttl = int.MaxValue; + if (!TryReadRecords(header.AnswerCount, ref ttl, ref responseReader, out List? answers) || + !TryReadRecords(header.AuthorityCount, ref ttl, ref responseReader, out List? authorities) || + !TryReadRecords(header.AdditionalRecordCount, ref ttl, ref responseReader, out List? additionals)) + { + return new SendQueryResult + { + Response = new DnsResponse(ArraySegment.Empty, header, queryStartedTime, queryStartedTime, null!, null!, null!), + Error = SendQueryError.MalformedResponse + }; + } + + DateTime expirationTime = + (answers.Count + authorities.Count + additionals.Count) > 0 ? queryStartedTime.AddSeconds(ttl) : queryStartedTime; + + SendQueryError validationError = ValidateResponse(header.ResponseCode, queryStartedTime, answers, authorities, ref expirationTime); + + // we transfer ownership of RawData to the response + DnsResponse response = new DnsResponse(responseReader.MessageBuffer, header, queryStartedTime, expirationTime, answers, authorities, additionals); + responseReader = default; // avoid disposing (and returning RawData to the pool) + + return new SendQueryResult { Response = response, Error = validationError }; + } + finally + { + responseReader.Dispose(); + } + + static bool TryReadRecords(int count, ref int ttl, ref DnsDataReader reader, out List records) + { + // Since `count` is attacker controlled, limit the initial capacity + // to 32 items to avoid excessive memory allocation. More than 32 + // records are unusual so we don't need to optimize for them. + records = new(Math.Min(count, 32)); + + for (int i = 0; i < count; i++) + { + if (!reader.TryReadResourceRecord(out var record)) + { + return false; + } + + ttl = Math.Min(ttl, record.Ttl); + records.Add(new DnsResourceRecord(record.Name, record.Type, record.Class, record.Ttl, record.Data)); + } + + return true; + } + } + + internal static bool GetNegativeCacheExpiration(DateTime createdAt, List authorities, out DateTime expiration) + { + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // Like normal answers negative answers have a time to live (TTL). As + // there is no record in the answer section to which this TTL can be + // applied, the TTL must be carried by another method. This is done by + // including the SOA record from the zone in the authority section of + // the reply. When the authoritative server creates this record its TTL + // is taken from the minimum of the SOA.MINIMUM field and SOA's TTL. + // This TTL decrements in a similar manner to a normal cached answer and + // upon reaching zero (0) indicates the cached negative answer MUST NOT + // be used again. + // + + DnsResourceRecord? soa = authorities.FirstOrDefault(r => r.Type == QueryType.SOA); + if (soa != null && DnsPrimitives.TryReadSoa(soa.Value.Data, out _, out _, out _, out _, out _, out _, out uint minimum, out _)) + { + expiration = createdAt.AddSeconds(Math.Min(minimum, soa.Value.Ttl)); + return true; + } + + expiration = default; + return false; + } + + internal static SendQueryError ValidateResponse(QueryResponseCode responseCode, DateTime createdAt, List answers, List authorities, ref DateTime expiration) + { + if (responseCode == QueryResponseCode.NoError) + { + if (answers.Count > 0) + { + return SendQueryError.NoError; + } + // + // RFC 2308 Section 2.2 - No Data + // + // NODATA is indicated by an answer with the RCODE set to NOERROR and no + // relevant answers in the answer section. The authority section will + // contain an SOA record, or there will be no NS records there. + // + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // A negative answer that resulted from a no data error (NODATA) should + // be cached such that it can be retrieved and returned in response to + // another query for the same that resulted in + // the cached negative response. + // + if (!authorities.Any(r => r.Type == QueryType.NS) && GetNegativeCacheExpiration(createdAt, authorities, out DateTime newExpiration)) + { + expiration = newExpiration; + // _cache.TryAdd(name, queryType, expiration, Array.Empty()); + } + return SendQueryError.NoData; + } + + if (responseCode == QueryResponseCode.NameError) + { + // + // RFC 2308 Section 5 - Caching Negative Answers + // + // A negative answer that resulted from a name error (NXDOMAIN) should + // be cached such that it can be retrieved and returned in response to + // another query for the same that resulted in the + // cached negative response. + // + if (GetNegativeCacheExpiration(createdAt, authorities, out DateTime newExpiration)) + { + expiration = newExpiration; + // _cache.TryAddNonexistent(name, expiration); + } + + return SendQueryError.NameError; + } + + return SendQueryError.ServerError; + } + + internal static (DnsDataReader reader, DnsMessageHeader header, SendQueryError sendError) SendDnsQueryCustomTransport(Func, int, int> callback, EncodedDomainName dnsSafeName, QueryType queryType) + { + byte[] buffer = ArrayPool.Shared.Rent(2048); + try + { + (ushort transactionId, int length) = EncodeQuestion(buffer, dnsSafeName, queryType); + length = callback(buffer, length); + + DnsDataReader responseReader = new DnsDataReader(new ArraySegment(buffer, 0, length), true); + + if (!responseReader.TryReadHeader(out DnsMessageHeader header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + return (default, default, SendQueryError.MalformedResponse); + } + + // transfer ownership of buffer to the caller + buffer = null!; + return (responseReader, header, SendQueryError.NoError); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + internal static async ValueTask<(DnsDataReader reader, DnsMessageHeader header)> SendDnsQueryCoreUdpAsync(IPEndPoint serverEndPoint, EncodedDomainName dnsSafeName, QueryType queryType, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(512); + try + { + Memory memory = buffer; + (ushort transactionId, int length) = EncodeQuestion(memory, dnsSafeName, queryType); + + using var socket = new Socket(serverEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + await socket.SendToAsync(memory.Slice(0, length), SocketFlags.None, serverEndPoint, cancellationToken).ConfigureAwait(false); + + DnsDataReader responseReader; + DnsMessageHeader header; + + while (true) + { + // Because this is UDP, the response must be in a single packet, + // if the response does not fit into a single UDP packet, the server will + // set the Truncated flag in the header, and we will need to retry with TCP. + int packetLength = await socket.ReceiveAsync(memory, SocketFlags.None, cancellationToken).ConfigureAwait(false); + + if (packetLength < DnsMessageHeader.HeaderLength) + { + continue; + } + + responseReader = new DnsDataReader(new ArraySegment(buffer, 0, packetLength), true); + if (!responseReader.TryReadHeader(out header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + // header mismatch, this is not a response to our query + continue; + } + + // ownership of the buffer is transferred to the reader, caller will dispose. + buffer = null!; + return (responseReader, header); + } + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + internal static async ValueTask<(DnsDataReader reader, DnsMessageHeader header, SendQueryError error)> SendDnsQueryCoreTcpAsync(IPEndPoint serverEndPoint, EncodedDomainName dnsSafeName, QueryType queryType, CancellationToken cancellationToken) + { + var buffer = ArrayPool.Shared.Rent(8 * 1024); + try + { + // When sending over TCP, the message is prefixed by 2B length + (ushort transactionId, int length) = EncodeQuestion(buffer.AsMemory(2), dnsSafeName, queryType); + BinaryPrimitives.WriteUInt16BigEndian(buffer, (ushort)length); + + using var socket = new Socket(serverEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + await socket.ConnectAsync(serverEndPoint, cancellationToken).ConfigureAwait(false); + await socket.SendAsync(buffer.AsMemory(0, length + 2), SocketFlags.None, cancellationToken).ConfigureAwait(false); + + int responseLength = -1; + int bytesRead = 0; + while (responseLength < 0 || bytesRead < responseLength + 2) + { + int read = await socket.ReceiveAsync(buffer.AsMemory(bytesRead), SocketFlags.None, cancellationToken).ConfigureAwait(false); + bytesRead += read; + + if (read == 0) + { + // connection closed before receiving complete response message + return (default, default, SendQueryError.MalformedResponse); + } + + if (responseLength < 0 && bytesRead >= 2) + { + responseLength = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2)); + + if (responseLength + 2 > buffer.Length) + { + // even though this is user-controlled pre-allocation, it is limited to + // 64 kB, so it should be fine. + var largerBuffer = ArrayPool.Shared.Rent(responseLength + 2); + Array.Copy(buffer, largerBuffer, bytesRead); + ArrayPool.Shared.Return(buffer); + buffer = largerBuffer; + } + } + } + + DnsDataReader responseReader = new DnsDataReader(new ArraySegment(buffer, 2, responseLength), true); + if (!responseReader.TryReadHeader(out DnsMessageHeader header) || + header.TransactionId != transactionId || + !header.IsResponse) + { + // header mismatch on TCP fallback + return (default, default, SendQueryError.MalformedResponse); + } + + // transfer ownership of buffer to the caller + buffer = null!; + return (responseReader, header, SendQueryError.NoError); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + private static (ushort id, int length) EncodeQuestion(Memory buffer, EncodedDomainName dnsSafeName, QueryType queryType) + { + DnsMessageHeader header = new DnsMessageHeader + { + TransactionId = (ushort)RandomNumberGenerator.GetInt32(ushort.MaxValue + 1), + QueryFlags = QueryFlags.RecursionDesired, + QueryCount = 1 + }; + + DnsDataWriter writer = new DnsDataWriter(buffer); + if (!writer.TryWriteHeader(header) || + !writer.TryWriteQuestion(dnsSafeName, queryType, QueryClass.Internet)) + { + // should never happen since we validated the name length before + throw new InvalidOperationException("Buffer too small"); + } + return (header.TransactionId, writer.Position); + } + + public void Dispose() + { + if (!_disposed) + { + _disposed = true; + + // Cancel all pending requests (if any). Note that we don't call CancelPendingRequests() but cancel + // the CTS directly. The reason is that CancelPendingRequests() would cancel the current CTS and create + // a new CTS. We don't want a new CTS in this case. + _pendingRequestsCts.Cancel(); + _pendingRequestsCts.Dispose(); + } + } + + private (CancellationTokenSource TokenSource, bool DisposeTokenSource, CancellationTokenSource PendingRequestsCts) PrepareCancellationTokenSource(CancellationToken cancellationToken) + { + // We need a CancellationTokenSource to use with the request. We always have the global + // _pendingRequestsCts to use, plus we may have a token provided by the caller, and we may + // have a timeout. If we have a timeout or a caller-provided token, we need to create a new + // CTS (we can't, for example, timeout the pending requests CTS, as that could cancel other + // unrelated operations). Otherwise, we can use the pending requests CTS directly. + + // Snapshot the current pending requests cancellation source. It can change concurrently due to cancellation being requested + // and it being replaced, and we need a stable view of it: if cancellation occurs and the caller's token hasn't been canceled, + // it's either due to this source or due to the timeout, and checking whether this source is the culprit is reliable whereas + // it's more approximate checking elapsed time. + CancellationTokenSource pendingRequestsCts = _pendingRequestsCts; + TimeSpan timeout = _options.Timeout; + + bool hasTimeout = timeout != System.Threading.Timeout.InfiniteTimeSpan; + if (hasTimeout || cancellationToken.CanBeCanceled) + { + CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, pendingRequestsCts.Token); + if (hasTimeout) + { + cts.CancelAfter(timeout); + } + + return (cts, DisposeTokenSource: true, pendingRequestsCts); + } + + return (pendingRequestsCts, DisposeTokenSource: false, pendingRequestsCts); + } + + private static EncodedDomainName GetNormalizedHostName(string name) + { + byte[] buffer = ArrayPool.Shared.Rent(256); + try + { + if (!DnsPrimitives.TryWriteQName(buffer, name, out _)) + { + throw new ArgumentException($"'{name}' is not a valid DNS name.", nameof(name)); + } + + List> labels = new(); + Memory memory = buffer.AsMemory(); + while (true) + { + int len = memory.Span[0]; + + if (len == 0) + { + // root label, we are finished + break; + } + + labels.Add(memory.Slice(1, len)); + memory = memory.Slice(len + 1); + } + + buffer = null!; // ownership transferred to the EncodedDomainName + return new EncodedDomainName(labels, buffer); + } + finally + { + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.cs new file mode 100644 index 00000000000..914ff9aac17 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResourceRecord.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. + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsResourceRecord +{ + public EncodedDomainName Name { get; } + public QueryType Type { get; } + public QueryClass Class { get; } + public int Ttl { get; } + public ReadOnlyMemory Data { get; } + + public DnsResourceRecord(EncodedDomainName name, QueryType type, QueryClass @class, int ttl, ReadOnlyMemory data) + { + Name = name; + Type = type; + Class = @class; + Ttl = ttl; + Data = data; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs new file mode 100644 index 00000000000..5a7fc8a0b52 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResponse.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct DnsResponse : IDisposable +{ + public DnsMessageHeader Header { get; } + public List Answers { get; } + public List Authorities { get; } + public List Additionals { get; } + public DateTime CreatedAt { get; } + public DateTime Expiration { get; } + public ArraySegment RawMessageBytes { get; private set; } + + public DnsResponse(ArraySegment rawData, DnsMessageHeader header, DateTime createdAt, DateTime expiration, List answers, List authorities, List additionals) + { + RawMessageBytes = rawData; + + Header = header; + CreatedAt = createdAt; + Expiration = expiration; + Answers = answers; + Authorities = authorities; + Additionals = additionals; + } + + public void Dispose() + { + if (RawMessageBytes.Array != null) + { + ArrayPool.Shared.Return(RawMessageBytes.Array); + } + + RawMessageBytes = default; // prevent further access to the raw data + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs new file mode 100644 index 00000000000..4c258cac3ac --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/EncodedDomainName.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal struct EncodedDomainName : IEquatable, IDisposable +{ + public IReadOnlyList> Labels { get; } + private byte[]? _pooledBuffer; + + public EncodedDomainName(List> labels, byte[]? pooledBuffer = null) + { + Labels = labels; + _pooledBuffer = pooledBuffer; + } + public override string ToString() + { + StringBuilder sb = new StringBuilder(); + + foreach (var label in Labels) + { + if (sb.Length > 0) + { + sb.Append('.'); + } + sb.Append(Encoding.ASCII.GetString(label.Span)); + } + + return sb.ToString(); + } + + public bool Equals(EncodedDomainName other) + { + if (Labels.Count != other.Labels.Count) + { + return false; + } + + for (int i = 0; i < Labels.Count; i++) + { + if (!Ascii.EqualsIgnoreCase(Labels[i].Span, other.Labels[i].Span)) + { + return false; + } + } + + return true; + } + + public override bool Equals(object? obj) + { + return obj is EncodedDomainName other && Equals(other); + } + + public override int GetHashCode() + { + HashCode hash = new HashCode(); + + foreach (var label in Labels) + { + foreach (byte b in label.Span) + { + hash.Add((byte)char.ToLower((char)b)); + } + } + + return hash.ToHashCode(); + } + + public void Dispose() + { + if (_pooledBuffer != null) + { + ArrayPool.Shared.Return(_pooledBuffer); + } + + _pooledBuffer = null; + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs new file mode 100644 index 00000000000..e09168d9552 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs @@ -0,0 +1,13 @@ +// 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.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal interface IDnsResolver +{ + ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default); + ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default); + ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default); +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs new file mode 100644 index 00000000000..c2ef13f922e --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs @@ -0,0 +1,36 @@ +// 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; +using System.Net.NetworkInformation; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class NetworkInfo +{ + // basic option to get DNS serves via NetworkInfo. We may get it directly later via proper APIs. + public static ResolverOptions GetOptions() + { + List servers = new List(); + + foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces()) + { + IPInterfaceProperties properties = nic.GetIPProperties(); + // avoid loopback, VPN etc. Should be re-visited. + + if (nic.NetworkInterfaceType == NetworkInterfaceType.Ethernet && nic.OperationalStatus == OperationalStatus.Up) + { + foreach (IPAddress server in properties.DnsAddresses) + { + IPEndPoint ep = new IPEndPoint(server, 53); // 53 is standard DNS port + if (!servers.Contains(ep)) + { + servers.Add(ep); + } + } + } + } + + return new ResolverOptions(servers); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs new file mode 100644 index 00000000000..732ca0216da --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryClass.cs @@ -0,0 +1,9 @@ +// 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.Dns.Resolver; + +internal enum QueryClass +{ + Internet = 1 +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs new file mode 100644 index 00000000000..02474b6cda1 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryFlags.cs @@ -0,0 +1,15 @@ +// 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.Dns.Resolver; + +[Flags] +internal enum QueryFlags : ushort +{ + RecursionAvailable = 0x0080, + RecursionDesired = 0x0100, + ResultTruncated = 0x0200, + HasAuthorityAnswer = 0x0400, + HasResponse = 0x8000, + ResponseCodeMask = 0x000F, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs new file mode 100644 index 00000000000..dd51c712112 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryResponseCode.cs @@ -0,0 +1,42 @@ +// 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.Dns.Resolver; + +/// +/// The response code (RCODE) in a DNS query response. +/// +internal enum QueryResponseCode : byte +{ + /// + /// No error condition + /// + NoError = 0, + + /// + /// The name server was unable to interpret the query. + /// + FormatError = 1, + + /// + /// The name server was unable to process this query due to a problem with the name server. + /// + ServerFailure = 2, + + /// + /// Meaningful only for responses from an authoritative name server, this + /// code signifies that the domain name referenced in the query does not + /// exist. + /// + NameError = 3, + + /// + /// The name server does not support the requested kind of query. + /// + NotImplemented = 4, + + /// + /// The name server refuses to perform the specified operation for policy reasons. + /// + Refused = 5, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs new file mode 100644 index 00000000000..2ccc898a5b7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/QueryType.cs @@ -0,0 +1,55 @@ +// 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.Dns.Resolver; + +/// +/// DNS Query Types. +/// +internal enum QueryType +{ + /// + /// A host address. + /// + A = 1, + + /// + /// An authoritative name server. + /// + NS = 2, + + /// + /// The canonical name for an alias. + /// + CNAME = 5, + + /// + /// Marks the start of a zone of authority. + /// + SOA = 6, + + /// + /// Mail exchange. + /// + MX = 15, + + /// + /// Text strings. + /// + TXT = 16, + + /// + /// IPv6 host address. (RFC 3596) + /// + AAAA = 28, + + /// + /// Location information. (RFC 2782) + /// + SRV = 33, + + /// + /// Wildcard match. + /// + All = 255 +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs new file mode 100644 index 00000000000..fbfdc5ae027 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs @@ -0,0 +1,48 @@ +// 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; +using System.Runtime.Versioning; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal static class ResolvConf +{ + [SupportedOSPlatform("linux")] + [SupportedOSPlatform("osx")] + public static ResolverOptions GetOptions() + { + return GetOptions(new StreamReader("/etc/resolv.conf")); + } + + public static ResolverOptions GetOptions(TextReader reader) + { + List serverList = new(); + + while (reader.ReadLine() is string line) + { + string[] tokens = line.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (line.StartsWith("nameserver")) + { + if (tokens.Length >= 2 && IPAddress.TryParse(tokens[1], out IPAddress? address)) + { + serverList.Add(new IPEndPoint(address, 53)); // 53 is standard DNS port + + if (serverList.Count == 3) + { + break; // resolv.conf manpage allow max 3 nameservers anyway + } + } + } + } + + if (serverList.Count == 0) + { + // If no nameservers are configured, fall back to the default behavior of using the system resolver configuration. + return NetworkInfo.GetOptions(); + } + + return new ResolverOptions(serverList); + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs new file mode 100644 index 00000000000..51d03f64bfd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs @@ -0,0 +1,31 @@ +// 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; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal sealed class ResolverOptions +{ + public IReadOnlyList Servers; + public int Attempts = 2; + public TimeSpan Timeout = TimeSpan.FromSeconds(3); + + // override for testing purposes + internal Func, int, int>? _transportOverride; + + public ResolverOptions(IReadOnlyList servers) + { + if (servers.Count == 0) + { + throw new ArgumentException("At least one DNS server is required.", nameof(servers)); + } + + Servers = servers; + } + + public ResolverOptions(IPEndPoint server) + { + Servers = new IPEndPoint[] { server }; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs new file mode 100644 index 00000000000..aed799ac8d6 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResultTypes.cs @@ -0,0 +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.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +internal record struct AddressResult(DateTime ExpiresAt, IPAddress Address); + +internal record struct ServiceResult(DateTime ExpiresAt, int Priority, int Weight, int Port, string Target, AddressResult[] Addresses); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs new file mode 100644 index 00000000000..3ba5632e207 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/SendQueryError.cs @@ -0,0 +1,47 @@ +// 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.Dns.Resolver; + +internal enum SendQueryError +{ + /// + /// DNS query was successful and returned response message with answers. + /// + NoError, + + /// + /// Server failed to respond to the query withing specified timeout. + /// + Timeout, + + /// + /// Server returned a response with an error code. + /// + ServerError, + + /// + /// Server returned a malformed response. + /// + MalformedResponse, + + /// + /// Server returned a response indicating that the name exists, but no data are available. + /// + NoData, + + /// + /// Server returned a response indicating the name does not exist. + /// + NameError, + + /// + /// Network-level error occurred during the query. + /// + NetworkError, + + /// + /// Internal error on part of the implementation. + /// + InternalError, +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 98b9de1fd68..7d05243f741 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -1,11 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using DnsClient; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; namespace Microsoft.Extensions.Hosting; @@ -46,7 +46,7 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); + services.TryAddSingleton(); services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore new file mode 100644 index 00000000000..0151cc4e360 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/.gitignore @@ -0,0 +1,2 @@ +# corpuses generated by the fuzzing engine +corpuses/** \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs new file mode 100644 index 00000000000..1b180d74b9d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs @@ -0,0 +1,44 @@ +// 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 System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +internal sealed class DnsResponseFuzzer : IFuzzer +{ + DnsResolver? _resolver; + byte[]? _buffer; + int _length; + + public void FuzzTarget(ReadOnlySpan data) + { + // lazy init + if (_resolver == null) + { + _buffer = new byte[4096]; + _resolver = new DnsResolver(new ResolverOptions(new IPEndPoint(IPAddress.Loopback, 53)) + { + Timeout = TimeSpan.FromSeconds(5), + Attempts = 1, + _transportOverride = (buffer, length) => + { + // the first two bytes are the random transaction ID, so we keep that + // and use the fuzzing payload for the rest of the DNS response + _buffer.AsSpan(0, Math.Min(_length, buffer.Length - 2)).CopyTo(buffer.Span.Slice(2)); + return _length + 2; + } + }); + } + + data.CopyTo(_buffer!); + _length = data.Length; + + // the _transportOverride makes the execution synchronous + ValueTask task = _resolver!.ResolveIPAddressesAsync("www.example.com", AddressFamily.InterNetwork, CancellationToken.None); + Debug.Assert(task.IsCompleted, "Task should be completed synchronously"); + task.GetAwaiter().GetResult(); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs new file mode 100644 index 00000000000..72f84b3c959 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/EncodedDomainNameFuzzer.cs @@ -0,0 +1,33 @@ +// 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.Dns.Tests.Fuzzing; + +internal sealed class EncodedDomainNameFuzzer : IFuzzer +{ + public void FuzzTarget(ReadOnlySpan data) + { + byte[] buffer = ArrayPool.Shared.Rent(data.Length); + try + { + data.CopyTo(buffer); + + // attempt to read at any offset to really stress the parser + for (int i = 0; i < data.Length; i++) + { + if (!DnsPrimitives.TryReadQName(buffer.AsMemory(0, data.Length), i, out EncodedDomainName name, out _)) + { + continue; + } + + // the domain name should be readable + _ = name.ToString(); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs new file mode 100644 index 00000000000..f657245a842 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/WriteDomainNameRoundTripFuzzer.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +internal sealed class WriteDomainNameRoundTripFuzzer : IFuzzer +{ + private static readonly System.Globalization.IdnMapping s_idnMapping = new(); + public void FuzzTarget(ReadOnlySpan data) + { + // first byte is the offset of the domain name, rest is the actual + // (simulated) DNS message payload + + byte[] buffer = ArrayPool.Shared.Rent(data.Length * 2); + + try + { + string domainName = Encoding.UTF8.GetString(data); + if (!DnsPrimitives.TryWriteQName(buffer, domainName, out int written)) + { + return; + } + + if (!DnsPrimitives.TryReadQName(buffer.AsMemory(0, written), 0, out EncodedDomainName name, out int read)) + { + return; + } + + if (read != written) + { + throw new InvalidOperationException($"Read {read} bytes, but wrote {written} bytes"); + } + + string readName = name.ToString(); + + if (!string.Equals(s_idnMapping.GetAscii(domainName).TrimEnd('.'), readName, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException($"Domain name mismatch: {readName} != {domainName}"); + } + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs new file mode 100644 index 00000000000..2ff9d86b2ce --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/GlobalUsings.cs @@ -0,0 +1,5 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +global using System.Buffers; +global using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs new file mode 100644 index 00000000000..4b4c8c99b4b --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/IFuzzer.cs @@ -0,0 +1,10 @@ +// 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.Dns.Tests.Fuzzing; + +public interface IFuzzer +{ + string Name => GetType().Name; + void FuzzTarget(ReadOnlySpan data); +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj new file mode 100644 index 00000000000..6572c27a1fa --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -0,0 +1,18 @@ + + + + $(DefaultTargetFramework) + enable + enable + Exe + + + + + + + + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs new file mode 100644 index 00000000000..22b1580d1ac --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Program.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using SharpFuzz; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing; + +public static class Program +{ + public static void Main(string[] args) + { + IFuzzer[] fuzzers = typeof(Program).Assembly.GetTypes() + .Where(t => t.IsClass && !t.IsAbstract) + .Where(t => t.GetInterfaces().Contains(typeof(IFuzzer))) + .Select(t => (IFuzzer)Activator.CreateInstance(t)!) + .OrderBy(f => f.Name, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + void PrintUsage() + { + Console.Error.WriteLine($""" + Usage: + DotnetFuzzing list + DotnetFuzzing [input file/directory] + // DotnetFuzzing prepare-onefuzz + + Available fuzzers: + {string.Join(Environment.NewLine, fuzzers.Select(f => $" {f.Name}"))} + """); + } + + if (args.Length == 0) + { + PrintUsage(); + return; + } + + string arg = args[0]; + IFuzzer? fuzzer = fuzzers.FirstOrDefault(f => string.Equals(f.Name, arg, StringComparison.OrdinalIgnoreCase)); + if (fuzzer == null) + { + Console.Error.WriteLine($"Unknown fuzzer: {arg}"); + PrintUsage(); + return; + } + + string? inputFiles = args.Length > 1 ? args[1] : null; + if (string.IsNullOrEmpty(inputFiles)) + { + // no input files, let the fuzzer generate + Fuzzer.LibFuzzer.Run(fuzzer.FuzzTarget); + return; + } + + string[] files = Directory.Exists(inputFiles) + ? Directory.GetFiles(inputFiles) + : [inputFiles]; + + foreach (string inputFile in files) + { + fuzzer.FuzzTarget(File.ReadAllBytes(inputFile)); + } + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/corpus-seed/DnsResponseFuzzer/ip-www.example.com new file mode 100644 index 0000000000000000000000000000000000000000..bb40cd100c651f4d67ef95ae5d0b10edf886eb34 GIT binary patch literal 141 zcmZo{U|?imVE_W=^73-_)QZI1f}B+5X=X_(b6#o*!vS50YC{(W5!OUQ6C)#*l;Y$fw#4kj+{DZSUI(HJSlAD(timeProvider) + .AddSingleton() .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30)) .BuildServiceProvider(); diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs index b58d9e2f4ec..ec21bf9fa9c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net; -using DnsClient; -using DnsClient.Protocol; +using System.Net.Sockets; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.ServiceDiscovery.Internal; using Xunit; @@ -19,88 +19,38 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; /// public class DnsSrvServiceEndpointResolverTests { - private sealed class FakeDnsClient : IDnsQuery + private sealed class FakeDnsResolver : IDnsResolver { - public Func>? QueryAsyncFunc { get; set; } + public Func>? ResolveIPAddressesAsyncFunc { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc!.Invoke(name, addressFamily, cancellationToken); - public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) - => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); - public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - } + public Func>? ResolveIPAddressesAsyncFunc2 { get; set; } - private sealed class FakeDnsQueryResponse : IDnsQueryResponse - { - public IReadOnlyList? Questions { get; set; } - public IReadOnlyList? Additionals { get; set; } - public IEnumerable? AllRecords { get; set; } - public IReadOnlyList? Answers { get; set; } - public IReadOnlyList? Authorities { get; set; } - public string? AuditTrail { get; set; } - public string? ErrorMessage { get; set; } - public bool HasError { get; set; } - public DnsResponseHeader? Header { get; set; } - public int MessageSize { get; set; } - public NameServer? NameServer { get; set; } - public DnsQuerySettings? Settings { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc2!.Invoke(name, cancellationToken); + + public Func>? ResolveServiceAsyncFunc { get; set; } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) => ResolveServiceAsyncFunc!.Invoke(name, cancellationToken); } [Fact] public async Task ResolveServiceEndpoint_DnsSrv() { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), - new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) - } - }; + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", []) + ]; - return Task.FromResult(response); + return ValueTask.FromResult(response); } }; var services = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); @@ -119,7 +69,7 @@ public async Task ResolveServiceEndpoint_DnsSrv() 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.Equal(new DnsEndPoint("srv-c", 7777), eps[2].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => { @@ -137,28 +87,17 @@ public async Task ResolveServiceEndpoint_DnsSrv() [Theory] public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst) { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new CNameRecord(new ResourceRecordInfo("srv-c", ResourceRecordType.AAAA, queryClass, 64, 0), DnsString.Parse("remotehost")), - new TxtRecord(new ResourceRecordInfo("srv-a", ResourceRecordType.TXT, queryClass, 64, 0), ["some txt values"], ["some txt utf8 values"]) - } - }; + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", []) + ]; - return Task.FromResult(response); + return ValueTask.FromResult(response); } }; var configSource = new MemoryConfigurationSource @@ -171,7 +110,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( }; var config = new ConfigurationBuilder().Add(configSource); var serviceCollection = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddSingleton(config.Build()) .AddServiceDiscoveryCore(); if (dnsFirst) @@ -211,7 +150,7 @@ public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing( 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.Equal(new DnsEndPoint("srv-c", 7777), eps[2].EndPoint); Assert.All(initialResult.EndpointSource.Endpoints, ep => { diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 24faf1d8abe..f6202d17d37 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -12,6 +12,10 @@ + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs new file mode 100644 index 00000000000..8c646ac18ee --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class CancellationTests : LoopbackDnsTestBase +{ + public CancellationTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task PreCanceledToken_Throws() + { + CancellationTokenSource cts = new CancellationTokenSource(); + cts.Cancel(); + + var ex = await Assert.ThrowsAnyAsync(async () => await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork, cts.Token)); + + Assert.Equal(cts.Token, ex.CancellationToken); + } + + [Fact] + public async Task CancellationInProgress_Throws() + { + CancellationTokenSource cts = new CancellationTokenSource(); + + var task = Assert.ThrowsAnyAsync(async () => await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork, cts.Token)); + + await DnsServer.ProcessUdpRequest(_ => + { + cts.Cancel(); + return Task.CompletedTask; + }); + + OperationCanceledException ex = await task; + Assert.Equal(cts.Token, ex.CancellationToken); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs new file mode 100644 index 00000000000..aad32fe785f --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataReaderTests.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsDataReaderTests +{ + [Fact] + public void ReadResourceRecord_Success() + { + // example A record for example.com + byte[] buffer = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsDataReader reader = new DnsDataReader(buffer); + Assert.True(reader.TryReadResourceRecord(out DnsResourceRecord record)); + + Assert.Equal("www.example.com", record.Name.ToString()); + Assert.Equal(QueryType.A, record.Type); + Assert.Equal(QueryClass.Internet, record.Class); + Assert.Equal(3600, record.Ttl); + Assert.Equal(4, record.Data.Length); + } + + [Fact] + public void ReadResourceRecord_Truncated_Fails() + { + // example A record for example.com + byte[] buffer = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + for (int i = 0; i < buffer.Length; i++) + { + DnsDataReader reader = new DnsDataReader(new ArraySegment(buffer, 0, i)); + Assert.False(reader.TryReadResourceRecord(out _)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs new file mode 100644 index 00000000000..b2039ce5a4c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsDataWriterTests.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsDataWriterTests +{ + [Fact] + public void WriteResourceRecord_Success() + { + // example A record for example.com + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsResourceRecord record = new DnsResourceRecord(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet, 3600, new byte[4]); + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteResourceRecord(record)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + [Fact] + public void WriteResourceRecord_Truncated_Fails() + { + // example A record for example.com + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01, + // TTL (3600) + 0x00, 0x00, 0x0e, 0x10, + // data length (4) + 0x00, 0x04, + // data (placeholder) + 0x00, 0x00, 0x00, 0x00 + ]; + + DnsResourceRecord record = new DnsResourceRecord(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet, 3600, new byte[4]); + + byte[] buffer = new byte[512]; + for (int i = 0; i < expected.Length; i++) + { + DnsDataWriter writer = new DnsDataWriter(buffer.AsMemory(0, i)); + Assert.False(writer.TryWriteResourceRecord(record)); + } + } + + [Fact] + public void WriteQuestion_Success() + { + // example question for example.com (A record) + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01 + ]; + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteQuestion(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + [Fact] + public void WriteQuestion_Truncated_Fails() + { + // example question for example.com (A record) + byte[] expected = [ + // name (www.example.com) + 0x03, 0x77, 0x77, 0x77, 0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00, + // type (A) + 0x00, 0x01, + // class (IN) + 0x00, 0x01 + ]; + + byte[] buffer = new byte[512]; + for (int i = 0; i < expected.Length; i++) + { + DnsDataWriter writer = new DnsDataWriter(buffer.AsMemory(0, i)); + Assert.False(writer.TryWriteQuestion(EncodeDomainName("www.example.com"), QueryType.A, QueryClass.Internet)); + } + } + + [Fact] + public void WriteHeader_Success() + { + // example header + byte[] expected = [ + // ID (0x1234) + 0x12, 0x34, + // Flags (0x5678) + 0x56, 0x78, + // Question count (1) + 0x00, 0x01, + // Answer count (0) + 0x00, 0x02, + // Authority count (0) + 0x00, 0x03, + // Additional count (0) + 0x00, 0x04 + ]; + + DnsMessageHeader header = new() + { + TransactionId = 0x1234, + QueryFlags = (QueryFlags)0x5678, + QueryCount = 1, + AnswerCount = 2, + AuthorityCount = 3, + AdditionalRecordCount = 4, + }; + + byte[] buffer = new byte[512]; + DnsDataWriter writer = new DnsDataWriter(buffer); + Assert.True(writer.TryWriteHeader(header)); + Assert.Equal(expected, buffer.AsSpan().Slice(0, writer.Position).ToArray()); + } + + private static EncodedDomainName EncodeDomainName(string name) + { + byte[] nameBuffer = new byte[512]; + Assert.True(DnsPrimitives.TryWriteQName(nameBuffer, name, out int nameLength)); + Assert.True(DnsPrimitives.TryReadQName(nameBuffer.AsMemory(0, nameLength), 0, out EncodedDomainName encodedDomainName, out _)); + return encodedDomainName; + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs new file mode 100644 index 00000000000..6733a553bad --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/DnsPrimitivesTests.cs @@ -0,0 +1,195 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class DnsPrimitivesTests +{ + public static TheoryData QNameData => new() + { + { "www.example.com", "\x0003www\x0007example\x0003com\x0000"u8.ToArray() }, + { "example.com", "\x0007example\x0003com\x0000"u8.ToArray() }, + { "com", "\x0003com\x0000"u8.ToArray() }, + { "example", "\x0007example\x0000"u8.ToArray() }, + { "www", "\x0003www\x0000"u8.ToArray() }, + { "a", "\x0001a\x0000"u8.ToArray() }, + }; + + [Theory] + [MemberData(nameof(QNameData))] + public void TryWriteQName_Success(string name, byte[] expected) + { + byte[] buffer = new byte[512]; + + Assert.True(DnsPrimitives.TryWriteQName(buffer, name, out int written)); + Assert.Equal(name.Length + 2, written); + Assert.Equal(expected, buffer.AsSpan().Slice(0, written).ToArray()); + } + + [Fact] + public void TryWriteQName_LabelTooLong_False() + { + byte[] buffer = new byte[512]; + + Assert.False(DnsPrimitives.TryWriteQName(buffer, new string('a', 70), out _)); + } + + [Fact] + public void TryWriteQName_BufferTooShort_Fails() + { + byte[] buffer = new byte[512]; + string name = "www.example.com"; + + for (int i = 0; i < name.Length + 2; i++) + { + Assert.False(DnsPrimitives.TryWriteQName(buffer.AsSpan(0, i), name, out _)); + } + } + + [Theory] + [InlineData("www.-0.com")] + [InlineData("www.-a.com")] + [InlineData("www.a-.com")] + [InlineData("www.a_a.com")] + [InlineData("www.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com")] // 64 occurrences of 'a' (too long) + [InlineData("www.a~a.com")] // 64 occurrences of 'a' (too long) + [InlineData("www..com")] + [InlineData("www..")] + public void TryWriteQName_InvalidName_ReturnsFalse(string name) + { + byte[] buffer = new byte[512]; + Assert.False(DnsPrimitives.TryWriteQName(buffer, name, out _)); + } + + [Fact] + public void TryWriteQName_ExplicitRoot_Success() + { + string name1 = "www.example.com"; + string name2 = "www.example.com."; + + byte[] buffer1 = new byte[512]; + byte[] buffer2 = new byte[512]; + + Assert.True(DnsPrimitives.TryWriteQName(buffer1, name1, out int written1)); + Assert.True(DnsPrimitives.TryWriteQName(buffer2, name2, out int written2)); + Assert.Equal(written1, written2); + Assert.Equal(buffer1.AsSpan().Slice(0, written1).ToArray(), buffer2.AsSpan().Slice(0, written2).ToArray()); + } + + [Theory] + [MemberData(nameof(QNameData))] + public void TryReadQName_Success(string expected, byte[] serialized) + { + Assert.True(DnsPrimitives.TryReadQName(serialized, 0, out EncodedDomainName actual, out int bytesRead)); + Assert.Equal(expected, actual.ToString()); + Assert.Equal(serialized.Length, bytesRead); + } + + [Fact] + public void TryReadQName_TruncatedData_Fails() + { + ReadOnlyMemory data = "\x0003www\x0007example\x0003com\x0000"u8.ToArray(); + + for (int i = 0; i < data.Length; i++) + { + Assert.False(DnsPrimitives.TryReadQName(data.Slice(0, i), 0, out _, out _)); + } + } + + [Fact] + public void TryReadQName_Pointer_Success() + { + // [7B padding], example.com. www->[ptr to example.com.] + Memory data = "padding\x0007example\x0003com\x0000\x0003www\x00\x07"u8.ToArray(); + data.Span[^2] = 0xc0; + + Assert.True(DnsPrimitives.TryReadQName(data, data.Length - 6, out EncodedDomainName actual, out int bytesRead)); + Assert.Equal("www.example.com", actual.ToString()); + Assert.Equal(6, bytesRead); + } + + [Fact] + public void TryReadQName_PointerTruncated_Fails() + { + // [7B padding], example.com. www->[ptr to example.com.] + Memory data = "padding\x0007example\x0003com\x0000\x0003www\x00\x07"u8.ToArray(); + data.Span[^2] = 0xc0; + + for (int i = 0; i < data.Length; i++) + { + Assert.False(DnsPrimitives.TryReadQName(data.Slice(0, i), data.Length - 6, out _, out _)); + } + } + + [Fact] + public void TryReadQName_ForwardPointer_Fails() + { + // www->[ptr to example.com], [7B padding], example.com. + Memory data = "\x03www\x00\x000dpadding\x0007example\x0003com\x00"u8.ToArray(); + data.Span[4] = 0xc0; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Fact] + public void TryReadQName_PointerToSelf_Fails() + { + // www->[ptr to www->...] + Memory data = "\x0003www\0\0"u8.ToArray(); + data.Span[4] = 0xc0; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Fact] + public void TryReadQName_PointerToPointer_Fails() + { + // com, example[->com], example2[->[->com]] + Memory data = "\x0003com\0\x0007example\0\0\x0008example2\0\0"u8.ToArray(); + data.Span[13] = 0xc0; + data.Span[14] = 0x00; // -> com + data.Span[24] = 0xc0; + data.Span[25] = 13; // -> -> com + + Assert.False(DnsPrimitives.TryReadQName(data, 15, out _, out _)); + } + + [Fact] + public void TryReadQName_ReservedBits() + { + Memory data = "\x0003www\x00c0"u8.ToArray(); + data.Span[0] = 0x40; + + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + + [Theory] + [InlineData(253)] + [InlineData(254)] + [InlineData(255)] + public void TryReadQName_NameTooLong(int length) + { + // longest possible label is 63 bytes + 1 byte for length + byte[] labelData = new byte[64]; + Array.Fill(labelData, (byte)'a'); + labelData[0] = 63; + + int remainder = length - 3 * 64; + + byte[] lastLabelData = new byte[remainder + 1]; + Array.Fill(lastLabelData, (byte)'a'); + lastLabelData[0] = (byte)remainder; + + byte[] data = Enumerable.Repeat(labelData, 3).SelectMany(x => x).Concat(lastLabelData).Concat(new byte[1]).ToArray(); + if (length > 253) + { + Assert.False(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + else + { + Assert.True(DnsPrimitives.TryReadQName(data, 0, out _, out _)); + } + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs new file mode 100644 index 00000000000..4789e21c575 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsServer.cs @@ -0,0 +1,331 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Globalization; +using System.Net; +using System.Net.Sockets; +using System.Text; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +internal sealed class LoopbackDnsServer : IDisposable +{ + private readonly Socket _dnsSocket; + private Socket? _tcpSocket; + + public IPEndPoint DnsEndPoint => (IPEndPoint)_dnsSocket.LocalEndPoint!; + + public LoopbackDnsServer() + { + _dnsSocket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _dnsSocket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); + } + + public void Dispose() + { + _dnsSocket.Dispose(); + _tcpSocket?.Dispose(); + } + + private static async Task ProcessRequestCore(IPEndPoint remoteEndPoint, ArraySegment message, Func action, Memory responseBuffer) + { + DnsDataReader reader = new DnsDataReader(message); + + if (!reader.TryReadHeader(out DnsMessageHeader header) || + !reader.TryReadQuestion(out var name, out var type, out var @class)) + { + return 0; + } + + LoopbackDnsResponseBuilder responseBuilder = new(name.ToString(), type, @class); + responseBuilder.TransactionId = header.TransactionId; + responseBuilder.Flags = header.QueryFlags | QueryFlags.HasResponse; + responseBuilder.ResponseCode = QueryResponseCode.NoError; + + await action(responseBuilder, remoteEndPoint); + + return responseBuilder.Write(responseBuffer); + } + + public async Task ProcessUdpRequest(Func action) + { + byte[] buffer = ArrayPool.Shared.Rent(512); + try + { + EndPoint remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); + SocketReceiveFromResult result = await _dnsSocket.ReceiveFromAsync(buffer, remoteEndPoint); + + int bytesWritten = await ProcessRequestCore((IPEndPoint)result.RemoteEndPoint, new ArraySegment(buffer, 0, result.ReceivedBytes), action, buffer.AsMemory(0, 512)); + + await _dnsSocket.SendToAsync(buffer.AsMemory(0, bytesWritten), SocketFlags.None, result.RemoteEndPoint); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public Task ProcessUdpRequest(Func action) + { + return ProcessUdpRequest((builder, _) => action(builder)); + } + + public async Task ProcessTcpRequest(Func action) + { + if (_tcpSocket is null) + { + _tcpSocket = new(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + _tcpSocket.Bind(new IPEndPoint(IPAddress.Loopback, ((IPEndPoint)_dnsSocket.LocalEndPoint!).Port)); + _tcpSocket.Listen(); + } + + using Socket tcpClient = await _tcpSocket.AcceptAsync(); + + byte[] buffer = ArrayPool.Shared.Rent(8 * 1024); + try + { + int bytesRead = 0; + int length = -1; + while (length < 0 || bytesRead < length + 2) + { + int toRead = length < 0 ? 2 : length + 2 - bytesRead; + int read = await tcpClient.ReceiveAsync(buffer.AsMemory(bytesRead, toRead), SocketFlags.None); + bytesRead += read; + + if (length < 0 && bytesRead >= 2) + { + length = BinaryPrimitives.ReadUInt16BigEndian(buffer.AsSpan(0, 2)); + } + } + + int bytesWritten = await ProcessRequestCore((IPEndPoint)tcpClient.RemoteEndPoint!, new ArraySegment(buffer, 2, length), action, buffer.AsMemory(2)); + BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(0, 2), (ushort)bytesWritten); + await tcpClient.SendAsync(buffer.AsMemory(0, bytesWritten + 2), SocketFlags.None); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public Task ProcessTcpRequest(Func action) + { + return ProcessTcpRequest((builder, _) => action(builder)); + } +} + +internal sealed class LoopbackDnsResponseBuilder +{ + private static readonly SearchValues s_domainNameValidChars = SearchValues.Create("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."); + + public LoopbackDnsResponseBuilder(string name, QueryType type, QueryClass @class) + { + Name = name; + Type = type; + Class = @class; + Questions.Add((name, type, @class)); + + if (name.AsSpan().ContainsAnyExcept(s_domainNameValidChars)) + { + throw new ArgumentException($"Invalid characters in domain name '{name}'"); + } + } + + public ushort TransactionId { get; set; } + public QueryFlags Flags { get; set; } + public QueryResponseCode ResponseCode { get; set; } + + public string Name { get; } + public QueryType Type { get; } + public QueryClass Class { get; } + + public List<(string, QueryType, QueryClass)> Questions { get; } = new List<(string, QueryType, QueryClass)>(); + public List Answers { get; } = new List(); + public List Authorities { get; } = new List(); + public List Additionals { get; } = new List(); + + public int Write(Memory responseBuffer) + { + DnsDataWriter writer = new(responseBuffer); + if (!writer.TryWriteHeader(new DnsMessageHeader + { + TransactionId = TransactionId, + QueryFlags = Flags | (QueryFlags)ResponseCode, + QueryCount = (ushort)Questions.Count, + AnswerCount = (ushort)Answers.Count, + AuthorityCount = (ushort)Authorities.Count, + AdditionalRecordCount = (ushort)Additionals.Count + })) + { + throw new InvalidOperationException("Failed to write header"); + } + + byte[] buffer = ArrayPool.Shared.Rent(512); + foreach (var (questionName, questionType, questionClass) in Questions) + { + if (!DnsPrimitives.TryWriteQName(buffer, questionName, out int length) || + !DnsPrimitives.TryReadQName(buffer.AsMemory(0, length), 0, out EncodedDomainName encodedName, out _)) + { + throw new InvalidOperationException("Failed to encode domain name"); + } + if (!writer.TryWriteQuestion(encodedName, questionType, questionClass)) + { + throw new InvalidOperationException("Failed to write question"); + } + } + ArrayPool.Shared.Return(buffer); + + foreach (var answer in Answers) + { + if (!writer.TryWriteResourceRecord(answer)) + { + throw new InvalidOperationException("Failed to write answer"); + } + } + + foreach (var authority in Authorities) + { + if (!writer.TryWriteResourceRecord(authority)) + { + throw new InvalidOperationException("Failed to write authority"); + } + } + + foreach (var additional in Additionals) + { + if (!writer.TryWriteResourceRecord(additional)) + { + throw new InvalidOperationException("Failed to write additional records"); + } + } + + return writer.Position; + } + + public byte[] GetMessageBytes() + { + byte[] buffer = ArrayPool.Shared.Rent(512); + try + { + int bytesWritten = Write(buffer.AsMemory(0, 512)); + return buffer.AsSpan(0, bytesWritten).ToArray(); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} + +internal static class LoopbackDnsServerExtensions +{ + private static readonly IdnMapping s_idnMapping = new IdnMapping(); + + private static EncodedDomainName EncodeDomainName(string name) + { + var encodedLabels = name.Split('.', StringSplitOptions.RemoveEmptyEntries).Select(label => (ReadOnlyMemory)Encoding.UTF8.GetBytes(s_idnMapping.GetAscii(label))) + .ToList(); + + return new EncodedDomainName(encodedLabels); + } + + public static List AddAddress(this List records, string name, int ttl, IPAddress address) + { + QueryType type = address.AddressFamily == AddressFamily.InterNetwork ? QueryType.A : QueryType.AAAA; + records.Add(new DnsResourceRecord(EncodeDomainName(name), type, QueryClass.Internet, ttl, address.GetAddressBytes())); + return records; + } + + public static List AddCname(this List records, string name, int ttl, string alias) + { + byte[] buff = new byte[256]; + if (!DnsPrimitives.TryWriteQName(buff, alias, out int length)) + { + throw new InvalidOperationException("Failed to encode domain name"); + } + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.CNAME, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } + + public static List AddService(this List records, string name, int ttl, ushort priority, ushort weight, ushort port, string target) + { + byte[] buff = new byte[256]; + + // https://www.rfc-editor.org/rfc/rfc2782 + if (!BinaryPrimitives.TryWriteUInt16BigEndian(buff, priority) || + !BinaryPrimitives.TryWriteUInt16BigEndian(buff.AsSpan(2), weight) || + !BinaryPrimitives.TryWriteUInt16BigEndian(buff.AsSpan(4), port) || + !DnsPrimitives.TryWriteQName(buff.AsSpan(6), target, out int length)) + { + throw new InvalidOperationException("Failed to encode SRV record"); + } + + length += 6; + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.SRV, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } + + public static List AddStartOfAuthority(this List records, string name, int ttl, string mname, string rname, uint serial, uint refresh, uint retry, uint expire, uint minimum) + { + byte[] buff = new byte[256]; + + // https://www.rfc-editor.org/rfc/rfc1035#section-3.3.13 + if (!DnsPrimitives.TryWriteQName(buff, mname, out int w1) || + !DnsPrimitives.TryWriteQName(buff.AsSpan(w1), rname, out int w2) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2), serial) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 4), refresh) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 8), retry) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 12), expire) || + !BinaryPrimitives.TryWriteUInt32BigEndian(buff.AsSpan(w1 + w2 + 16), minimum)) + { + throw new InvalidOperationException("Failed to encode SOA record"); + } + + int length = w1 + w2 + 20; + + records.Add(new DnsResourceRecord(EncodeDomainName(name), QueryType.SOA, QueryClass.Internet, ttl, buff.AsMemory(0, length))); + return records; + } +} + +internal static class DnsDataWriterExtensions +{ + internal static bool TryWriteResourceRecord(this DnsDataWriter writer, DnsResourceRecord record) + { + if (!TryWriteDomainName(writer, record.Name) || + !writer.TryWriteUInt16((ushort)record.Type) || + !writer.TryWriteUInt16((ushort)record.Class) || + !writer.TryWriteUInt32((uint)record.Ttl) || + !writer.TryWriteUInt16((ushort)record.Data.Length) || + !writer.TryWriteRawData(record.Data.Span)) + { + return false; + } + + return true; + } + + internal static bool TryWriteDomainName(this DnsDataWriter writer, EncodedDomainName name) + { + foreach (var label in name.Labels) + { + if (label.Length > 63) + { + throw new InvalidOperationException("Label length exceeds maximum of 63 bytes"); + } + + if (!writer.TryWriteByte((byte)label.Length) || + !writer.TryWriteRawData(label.Span)) + { + return false; + } + } + + // root label + return writer.TryWriteByte(0); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs new file mode 100644 index 00000000000..f76621db93c --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using Microsoft.Extensions.Time.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public abstract class LoopbackDnsTestBase : IDisposable +{ + protected readonly ITestOutputHelper Output; + + internal LoopbackDnsServer DnsServer { get; } + private readonly Lazy _resolverLazy; + internal DnsResolver Resolver => _resolverLazy.Value; + internal ResolverOptions Options { get; } + protected readonly FakeTimeProvider TimeProvider; + + public LoopbackDnsTestBase(ITestOutputHelper output) + { + Output = output; + DnsServer = new(); + TimeProvider = new(); + Options = new([DnsServer.DnsEndPoint]) + { + Timeout = TimeSpan.FromSeconds(5), + Attempts = 1, + }; + _resolverLazy = new(InitializeResolver); + } + + DnsResolver InitializeResolver() + { + ServiceCollection services = new(); + services.AddXunitLogging(Output); + + // construct DnsResolver manually via internal constructor which accepts ResolverOptions + var resolver = new DnsResolver(TimeProvider, services.BuildServiceProvider().GetRequiredService>(), Options); + return resolver; + } + + public void Dispose() + { + DnsServer.Dispose(); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs new file mode 100644 index 00000000000..281ffbecd24 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolvConfTests +{ + [Fact] + public void GetOptions() + { + var contents = @" +nameserver 10.96.0.10 +search default.svc.cluster.local svc.cluster.local cluster.local +options ndots:5 +@"; + + var reader = new StringReader(contents); + ResolverOptions options = ResolvConf.GetOptions(reader); + + IPEndPoint ipAddress = Assert.Single(options.Servers); + Assert.Equal(new IPEndPoint(IPAddress.Parse("10.96.0.10"), 53), ipAddress); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs new file mode 100644 index 00000000000..b87e1362f3d --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs @@ -0,0 +1,307 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolveAddressesTests : LoopbackDnsTestBase +{ + public ResolveAddressesTests(ITestOutputHelper output) : base(output) + { + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveIPv4_NoData_Success(bool includeSoa) + { + string hostName = "nodata.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + if (includeSoa) + { + builder.Authorities.AddStartOfAuthority("ns.com", 240, "ns.com", "admin.ns.com", 1, 900, 180, 6048000, 3600); + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task ResolveIPv4_NoSuchName_Success(bool includeSoa) + { + string hostName = "nosuchname.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.ResponseCode = QueryResponseCode.NameError; + if (includeSoa) + { + builder.Authorities.AddStartOfAuthority("ns.com", 240, "ns.com", "admin.ns.com", 1, 900, 180, 6048000, 3600); + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Theory] + [InlineData("www.resolveipv4.com")] + [InlineData("www.resolveipv4.com.")] + [InlineData("www.ř.com")] + public async Task ResolveIPv4_Simple_Success(string name) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddAddress(name, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_InOrder_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-in-order.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example3.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_OutOfOrder_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-out-of-order.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example3.com", 3600, address); + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task ResolveIPv4_Aliases_Loop_ReturnsEmpty() + { + string hostName = "alias-loop2.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname("www.example3.com", 3600, hostName); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Aliases_Loop_Reverse_ReturnsEmpty() + { + string hostName = "alias-loop2.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname("www.example3.com", 3600, hostName); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Alias_And_Address() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-address.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddAddress("www.example2.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_DuplicateAlias() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "duplicate-alias.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example4.com"); + builder.Answers.AddAddress("www.example2.com", 3600, address); + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIPv4_Aliases_NotFound_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "alias-no-found.test"; + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddCname(hostName, 3600, "www.example2.com"); + builder.Answers.AddCname("www.example2.com", 3600, "www.example3.com"); + + // extra address in the answer not connected to the above + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + Assert.Empty(results); + } + + [Fact] + public async Task ResolveIP_InvalidAddressFamily_Throws() + { + await Assert.ThrowsAsync(async () => await Resolver.ResolveIPAddressesAsync("invalid-af.test", AddressFamily.Unknown)); + } + + [Theory] + [InlineData(AddressFamily.InterNetwork, "127.0.0.1")] + [InlineData(AddressFamily.InterNetworkV6, "::1")] + public async Task ResolveIP_Localhost_ReturnsLoopback(AddressFamily family, string addressAsString) + { + IPAddress address = IPAddress.Parse(addressAsString); + AddressResult[] results = await Resolver.ResolveIPAddressesAsync("localhost", family); + AddressResult result = Assert.Single(results); + + Assert.Equal(address, result.Address); + } + + [Fact] + public async Task Resolve_Timeout_ReturnsEmpty() + { + Options.Timeout = TimeSpan.FromSeconds(1); + AddressResult[] result = await Resolver.ResolveIPAddressesAsync("timeout-empty.test", AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Theory] + [InlineData("not-example.com", (int)QueryType.A, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.AAAA, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.A, 0)] + public async Task Resolve_QuestionMismatch_ReturnsEmpty(string name, int type, int @class) + { + Options.Timeout = TimeSpan.FromSeconds(1); + + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Questions[0] = (name, (QueryType)type, (QueryClass)@class); + builder.Answers.AddAddress("www.example4.com", 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync("example.com", AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Fact] + public async Task Resolve_HeaderMismatch_Ignores() + { + string name = "header-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(5); + + SemaphoreSlim responseSemaphore = new SemaphoreSlim(0, 1); + SemaphoreSlim requestSemaphore = new SemaphoreSlim(0, 1); + + IPEndPoint clientAddress = null!; + + IPAddress address = IPAddress.Parse("172.213.245.111"); + ushort transactionId = 0x1234; + _ = DnsServer.ProcessUdpRequest((builder, clientAddr) => + { + clientAddress = clientAddr; + transactionId = (ushort)(builder.TransactionId + 1); + + builder.Answers.AddAddress(name, 3600, address); + requestSemaphore.Release(); + return responseSemaphore.WaitAsync(); + }); + + ValueTask task = Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + + await requestSemaphore.WaitAsync().WaitAsync(Options.Timeout); + + using Socket socket = new Socket(clientAddress.AddressFamily, SocketType.Dgram, ProtocolType.Udp); + LoopbackDnsResponseBuilder responseBuilder = new LoopbackDnsResponseBuilder(name, QueryType.A, QueryClass.Internet) + { + TransactionId = transactionId, + ResponseCode = QueryResponseCode.NoError + }; + + responseBuilder.Questions.Add((name, QueryType.A, QueryClass.Internet)); + responseBuilder.Answers.AddAddress(name, 3600, IPAddress.Loopback); + socket.SendTo(responseBuilder.GetMessageBytes(), clientAddress); + + responseSemaphore.Release(); + + AddressResult[] results = await task; + AddressResult result = Assert.Single(results); + + Assert.Equal(address, result.Address); + } +} \ No newline at end of file diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs new file mode 100644 index 00000000000..e1cd1df2959 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class ResolveServiceTests : LoopbackDnsTestBase +{ + public ResolveServiceTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task ResolveService_Simple_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Answers.AddService("_s0._tcp.example.com", 3600, 1, 2, 8080, "www.example.com"); + builder.Additionals.AddAddress("www.example.com", 3600, address); + return Task.CompletedTask; + }); + + ServiceResult[] results = await Resolver.ResolveServiceAsync("_s0._tcp.example.com"); + + ServiceResult result = Assert.Single(results); + Assert.Equal("www.example.com", result.Target); + Assert.Equal(1, result.Priority); + Assert.Equal(2, result.Weight); + Assert.Equal(8080, result.Port); + + AddressResult addressResult = Assert.Single(result.Addresses); + Assert.Equal(address, addressResult.Address); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs new file mode 100644 index 00000000000..800905d1ac5 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -0,0 +1,309 @@ +// 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; +using System.Net.Sockets; +using Xunit; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class RetryTests : LoopbackDnsTestBase +{ + public RetryTests(ITestOutputHelper output) : base(output) + { + Options.Attempts = 3; + } + + private Task SetupUdpProcessFunction(LoopbackDnsServer server, Func func) + { + return Task.Run(async () => + { + try + { + while (true) + { + await server.ProcessUdpRequest(func); + } + } + catch (Exception ex) + { + Output.WriteLine($"UDP server stopped with exception: {ex}"); + // Test teardown closed the socket, ignore + } + }); + } + + private Task SetupUdpProcessFunction(Func func) + { + return SetupUdpProcessFunction(DnsServer, func); + } + + [Fact] + public async Task Retry_Simple_Success() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "retry-simple-success.com"; + + int attempt = 0; + + Task t = SetupUdpProcessFunction(builder => + { + attempt++; + if (attempt == Options.Attempts) + { + builder.Answers.AddAddress(hostName, 3600, address); + } + else + { + builder.ResponseCode = QueryResponseCode.ServerFailure; + } + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum PersistentErrorType + { + NotImplemented, + Refused, + MalformedResponse + } + + [Theory] + [InlineData(PersistentErrorType.NotImplemented)] + [InlineData(PersistentErrorType.Refused)] + [InlineData(PersistentErrorType.MalformedResponse)] + public async Task PersistentErrorsResponseCode_FailoverToNextServer(PersistentErrorType type) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.persistent.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + switch (type) + { + case PersistentErrorType.NotImplemented: + builder.ResponseCode = QueryResponseCode.NotImplemented; + break; + + case PersistentErrorType.Refused: + builder.ResponseCode = QueryResponseCode.Refused; + break; + + case PersistentErrorType.MalformedResponse: + builder.ResponseCode = (QueryResponseCode)0xFF; + break; + } + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + Assert.Equal(1, primaryAttempt); + Assert.Equal(1, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum DefinitveAnswerType + { + NoError, + NoData, + NameError, + } + + [Theory] + [InlineData(DefinitveAnswerType.NoError, false)] + [InlineData(DefinitveAnswerType.NoData, false)] + [InlineData(DefinitveAnswerType.NoData, true)] + [InlineData(DefinitveAnswerType.NameError, false)] + [InlineData(DefinitveAnswerType.NameError, true)] + public async Task DefinitiveAnswers_NoRetryOrFailover(DefinitveAnswerType type, bool additionalData) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.retry.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + switch (type) + { + case DefinitveAnswerType.NoError: + builder.ResponseCode = QueryResponseCode.NoError; + builder.Answers.AddAddress(hostName, 3600, address); + break; + + case DefinitveAnswerType.NoData: + builder.ResponseCode = QueryResponseCode.NoError; + break; + + case DefinitveAnswerType.NameError: + builder.ResponseCode = QueryResponseCode.NameError; + break; + } + + if (additionalData) + { + builder.Authorities.AddStartOfAuthority(hostName, 300, "ns1.example.com", "hostmaster.example.com", 2023101001, 1, 3600, 300, 86400); + } + + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.ResponseCode = QueryResponseCode.Refused; + return Task.CompletedTask; + }); + + Assert.Equal(1, primaryAttempt); + Assert.Equal(0, secondaryAttempt); + + if (type == DefinitveAnswerType.NoError) + { + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + else + { + Assert.Empty(results); + } + } + + [Fact] + public async Task ExhaustedRetries_FailoverToNextServer() + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "ExhaustedRetriesFailoverToNextServer"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + builder => + { + primaryAttempt++; + builder.ResponseCode = QueryResponseCode.ServerFailure; + return Task.CompletedTask; + }, + builder => + { + secondaryAttempt++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + Assert.Equal(Options.Attempts, primaryAttempt); + Assert.Equal(1, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + public enum TransientErrorType + { + Timeout, + ServerFailure, + // TODO: simulate NetworkErrors + } + + [Theory] + [InlineData(TransientErrorType.Timeout)] + [InlineData(TransientErrorType.ServerFailure)] + public async Task TransientError_RetryOnSameServer(TransientErrorType type) + { + IPAddress address = IPAddress.Parse("172.213.245.111"); + string hostName = "www.transient.com"; + + int primaryAttempt = 0; + int secondaryAttempt = 0; + + AddressResult[] results = await RunWithFallbackServerHelper(hostName, + async builder => + { + primaryAttempt++; + if (primaryAttempt == 1) + { + switch (type) + { + case TransientErrorType.Timeout: + await Task.Delay(Options.Timeout.Multiply(1.5)); + builder.Answers.AddAddress(hostName, 3600, address); + break; + + case TransientErrorType.ServerFailure: + builder.ResponseCode = QueryResponseCode.ServerFailure; + break; + } + } + else + { + builder.Answers.AddAddress(hostName, 3600, address); + } + }, + builder => + { + secondaryAttempt++; + builder.ResponseCode = QueryResponseCode.Refused; + return Task.CompletedTask; + }); + + Assert.Equal(2, primaryAttempt); + Assert.Equal(0, secondaryAttempt); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + private async Task RunWithFallbackServerHelper(string name, Func primaryHandler, Func fallbackHandler) + { + Task t = SetupUdpProcessFunction(primaryHandler); + using LoopbackDnsServer fallbackServer = new LoopbackDnsServer(); + Task t2 = SetupUdpProcessFunction(fallbackServer, fallbackHandler); + + Options.Servers = [DnsServer.DnsEndPoint, fallbackServer.DnsEndPoint]; + + return await Resolver.ResolveIPAddressesAsync(name, AddressFamily.InterNetwork); + } + + [Fact] + public async Task NameError_NoRetry() + { + int counter = 0; + Task t = SetupUdpProcessFunction(builder => + { + counter++; + // authoritative answer that the name does not exist + builder.ResponseCode = QueryResponseCode.NameError; + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync("nameerror-noretry", AddressFamily.InterNetwork); + + Assert.Empty(results); + Assert.Equal(1, counter); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs new file mode 100644 index 00000000000..40841e3d11a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; +using System.Net; +using System.Net.Sockets; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; + +public class TcpFailoverTests : LoopbackDnsTestBase +{ + public TcpFailoverTests(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task TcpFailover_Simple_Success() + { + string hostName = "tcp-simple.test"; + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + + AddressResult res = Assert.Single(results); + Assert.Equal(address, res.Address); + Assert.Equal(TimeProvider.GetUtcNow().DateTime.AddSeconds(3600), res.ExpiresAt); + } + + [Fact] + public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() + { + string hostName = "tcp-server-closes.test"; + Options.Attempts = 1; + Options.Timeout = TimeSpan.FromSeconds(60); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + Task serverTask = DnsServer.ProcessTcpRequest(builder => + { + throw new InvalidOperationException("This forces closing the socket without writing any data"); + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork).AsTask().WaitAsync(TimeSpan.FromSeconds(10)); + Assert.Empty(results); + + await Assert.ThrowsAsync(() => serverTask); + } + + [Fact] + public async Task TcpFailover_TcpNotAvailable_EmptyResult() + { + string hostName = "tcp-not-available.test"; + Options.Attempts = 1; + Options.Timeout = TimeSpan.FromMilliseconds(100000); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + AddressResult[] results = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(results); + } + + [Fact] + public async Task TcpFailover_HeaderMismatch_ReturnsEmpty() + { + string hostName = "tcp-header-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(1); + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.TransactionId++; + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(result); + } + + [Theory] + [InlineData("not-example.com", (int)QueryType.A, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.AAAA, (int)QueryClass.Internet)] + [InlineData("example.com", (int)QueryType.A, 0)] + public async Task TcpFailover_QuestionMismatch_ReturnsEmpty(string name, int type, int @class) + { + string hostName = "tcp-question-mismatch.test"; + Options.Timeout = TimeSpan.FromSeconds(1); + IPAddress address = IPAddress.Parse("172.213.245.111"); + + _ = DnsServer.ProcessUdpRequest(builder => + { + builder.Flags |= QueryFlags.ResultTruncated; + return Task.CompletedTask; + }); + + _ = DnsServer.ProcessTcpRequest(builder => + { + builder.Questions[0] = (name, (QueryType)type, (QueryClass)@class); + builder.Answers.AddAddress(hostName, 3600, address); + return Task.CompletedTask; + }); + + AddressResult[] result = await Resolver.ResolveIPAddressesAsync(hostName, AddressFamily.InterNetwork); + Assert.Empty(result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index 96fd46d47ea..c2751823c65 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -6,10 +6,11 @@ using Xunit; using Yarp.ReverseProxy.Configuration; using System.Net; -using DnsClient; -using DnsClient.Protocol; +using System.Net.Sockets; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; @@ -231,7 +232,10 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Dns() { + DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance); + await using var services = new ServiceCollection() + .AddSingleton(resolver) .AddServiceDiscoveryCore() .AddDnsServiceEndpointProvider() .BuildServiceProvider(); @@ -265,32 +269,22 @@ public async Task ServiceDiscoveryDestinationResolverTests_Dns() [Fact] public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() { - var dnsClientMock = new FakeDnsClient + var dnsClientMock = new FakeDnsResolver { - QueryAsyncFunc = (query, queryType, queryClass, cancellationToken) => + ResolveServiceAsyncFunc = (name, cancellationToken) => { - var response = new FakeDnsQueryResponse - { - Answers = new List - { - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 66, 8888, DnsString.Parse("srv-a")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 9999, DnsString.Parse("srv-b")), - new SrvRecord(new ResourceRecordInfo(query, ResourceRecordType.SRV, queryClass, 123, 0), 99, 62, 7777, DnsString.Parse("srv-c")) - }, - Additionals = new List - { - new ARecord(new ResourceRecordInfo("srv-a", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Parse("10.10.10.10")), - new ARecord(new ResourceRecordInfo("srv-b", ResourceRecordType.AAAA, queryClass, 64, 0), IPAddress.IPv6Loopback), - new ARecord(new ResourceRecordInfo("srv-c", ResourceRecordType.A, queryClass, 64, 0), IPAddress.Loopback), - } - }; - - return Task.FromResult(response); + ServiceResult[] response = [ + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 66, 8888, "srv-a", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Parse("10.10.10.10"))]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 9999, "srv-b", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.IPv6Loopback)]), + new ServiceResult(DateTime.UtcNow.AddSeconds(60), 99, 62, 7777, "srv-c", [new AddressResult(DateTime.UtcNow.AddSeconds(64), IPAddress.Loopback)]) + ]; + + return ValueTask.FromResult(response); } }; await using var services = new ServiceCollection() - .AddSingleton(dnsClientMock) + .AddSingleton(dnsClientMock) .AddServiceDiscoveryCore() .AddDnsSrvServiceEndpointProvider(options => options.QuerySuffix = ".ns") .BuildServiceProvider(); @@ -313,56 +307,17 @@ public async Task ServiceDiscoveryDestinationResolverTests_DnsSrv() a => Assert.Equal("https://127.0.0.1:7777/", a)); } - private sealed class FakeDnsClient : IDnsQuery + private sealed class FakeDnsResolver : IDnsResolver { - public Func>? QueryAsyncFunc { get; set; } - - public IDnsQueryResponse Query(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse Query(DnsQuestion question, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryAsync(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) - => QueryAsyncFunc!(query, queryType, queryClass, cancellationToken); - public Task QueryAsync(DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryAsync(DnsQuestion question, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryCache(string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryReverse(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryReverseAsync(IPAddress ipAddress, DnsQueryAndServerOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServer(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, DnsQuestion question, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerAsync(IReadOnlyCollection servers, string query, QueryType queryType, QueryClass queryClass = QueryClass.IN, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress) => throw new NotImplementedException(); - public IDnsQueryResponse QueryServerReverse(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task QueryServerReverseAsync(IReadOnlyCollection servers, IPAddress ipAddress, DnsQueryOptions queryOptions, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - } + public Func>? ResolveIPAddressesAsyncFunc { get; set; } + public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc!.Invoke(name, addressFamily, cancellationToken); - private sealed class FakeDnsQueryResponse : IDnsQueryResponse - { - public IReadOnlyList? Questions { get; set; } - public IReadOnlyList? Additionals { get; set; } - public IEnumerable? AllRecords { get; set; } - public IReadOnlyList? Answers { get; set; } - public IReadOnlyList? Authorities { get; set; } - public string? AuditTrail { get; set; } - public string? ErrorMessage { get; set; } - public bool HasError { get; set; } - public DnsResponseHeader? Header { get; set; } - public int MessageSize { get; set; } - public NameServer? NameServer { get; set; } - public DnsQuerySettings? Settings { get; set; } + public Func>? ResolveIPAddressesAsyncFunc2 { get; set; } + + public ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) => ResolveIPAddressesAsyncFunc2!.Invoke(name, cancellationToken); + + public Func>? ResolveServiceAsyncFunc { get; set; } + + public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) => ResolveServiceAsyncFunc!.Invoke(name, cancellationToken); } } From f3141737a2c1b7557a1b7bc07073ac72cafc6a93 Mon Sep 17 00:00:00 2001 From: dotnet bot Date: Wed, 25 Jun 2025 20:19:25 +0200 Subject: [PATCH 71/77] Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2737112 (#10028) * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736879 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736911 * Localized file check-in by OneLocBuild Task: Build definition ID 1309: Build ID 2736942 From 4b296e42b98705569b91178e0d39092a4931a20f Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:41:21 +0200 Subject: [PATCH 72/77] Fallback to previous DNS service discovery (#10140) * Fallback to previous DNS service discovery * Code review feedback --- .../FallbackDnsResolver.cs | 102 ++++++++++++++++++ ...oft.Extensions.ServiceDiscovery.Dns.csproj | 1 + .../Resolver/DnsResolver.cs | 2 +- .../Resolver/IDnsResolver.cs | 3 - ...DiscoveryDnsServiceCollectionExtensions.cs | 28 ++++- 5 files changed, 131 insertions(+), 5 deletions(-) create mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs new file mode 100644 index 00000000000..1cdcab2f05d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/FallbackDnsResolver.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using DnsClient; +using DnsClient.Protocol; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed class FallbackDnsResolver : IDnsResolver +{ + private readonly LookupClient _lookupClient; + private readonly IOptionsMonitor _options; + private readonly TimeProvider _timeProvider; + + public FallbackDnsResolver(LookupClient lookupClient, IOptionsMonitor options, TimeProvider timeProvider) + { + _lookupClient = lookupClient; + _options = options; + _timeProvider = timeProvider; + } + + private TimeSpan DefaultRefreshPeriod => _options.CurrentValue.DefaultRefreshPeriod; + + public async ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default) + { + DateTime expiresAt = _timeProvider.GetUtcNow().DateTime.Add(DefaultRefreshPeriod); + var addresses = await System.Net.Dns.GetHostAddressesAsync(name, cancellationToken).ConfigureAwait(false); + + var results = new AddressResult[addresses.Length]; + + for (int i = 0; i < addresses.Length; i++) + { + results[i] = new AddressResult + { + Address = addresses[i], + ExpiresAt = expiresAt + }; + } + + return results; + } + + public async ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) + { + DateTime now = _timeProvider.GetUtcNow().DateTime; + var queryResult = await _lookupClient.QueryAsync(name, DnsClient.QueryType.SRV, cancellationToken: cancellationToken).ConfigureAwait(false); + if (queryResult.HasError) + { + throw CreateException(name, queryResult.ErrorMessage); + } + + var lookupMapping = new Dictionary>(); + foreach (var record in queryResult.Additionals.OfType()) + { + if (!lookupMapping.TryGetValue(record.DomainName, out var addresses)) + { + addresses = new List(); + lookupMapping[record.DomainName] = addresses; + } + + addresses.Add(new AddressResult + { + Address = record.Address, + ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive)) + }); + } + + var srvRecords = queryResult.Answers.OfType().ToList(); + + var results = new ServiceResult[srvRecords.Count]; + for (int i = 0; i < srvRecords.Count; i++) + { + var record = srvRecords[i]; + + results[i] = new ServiceResult + { + ExpiresAt = now.Add(TimeSpan.FromSeconds(record.TimeToLive)), + Priority = record.Priority, + Weight = record.Weight, + Port = record.Port, + Target = record.Target, + Addresses = lookupMapping.TryGetValue(record.Target, out var addresses) + ? addresses.ToArray() + : Array.Empty() + }; + } + + return results; + } + + private static InvalidOperationException CreateException(string dnsName, string errorMessage) + { + var msg = errorMessage switch + { + { Length: > 0 } => $"No DNS SRV records were found for DNS name '{dnsName}': {errorMessage}.", + _ => $"No DNS SRV records were found for DNS name '{dnsName}'", + }; + return new InvalidOperationException(msg); + } +} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 3aba9b3aaea..6d8bbb47842 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs index 5722356a1c3..bc290c6b907 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -143,7 +143,7 @@ public async ValueTask ResolveIPAddressesAsync(string name, Can return results; } - public ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) + internal ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default) { ObjectDisposedException.ThrowIf(_disposed, this); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs index e09168d9552..080fe3be8de 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/IDnsResolver.cs @@ -1,13 +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.Net.Sockets; - namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; internal interface IDnsResolver { - ValueTask ResolveIPAddressesAsync(string name, AddressFamily addressFamily, CancellationToken cancellationToken = default); ValueTask ResolveIPAddressesAsync(string name, CancellationToken cancellationToken = default); ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default); } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 7d05243f741..42f220445b1 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -46,11 +46,37 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); - services.TryAddSingleton(); + + if (!GetDnsClientFallbackFlag()) + { + services.TryAddSingleton(); + } + else + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } + services.AddSingleton(); var options = services.AddOptions(); options.Configure(o => configureOptions?.Invoke(o)); return services; + + static bool GetDnsClientFallbackFlag() + { + if (AppContext.TryGetSwitch("Microsoft.Extensions.ServiceDiscovery.Dns.UseDnsClientFallback", out var value)) + { + return value; + } + + var envVar = Environment.GetEnvironmentVariable("MICROSOFT_EXTENSIONS_SERVICE_DISCOVERY_DNS_USE_DNSCLIENT_FALLBACK"); + if (envVar is not null && (envVar.Equals("true", StringComparison.OrdinalIgnoreCase) || envVar.Equals("1"))) + { + return true; + } + + return false; + } } /// From 81b24582150092198d026850e781d892ea07e32b Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Mon, 25 Aug 2025 15:45:17 -0700 Subject: [PATCH 73/77] Build ServiceDiscovery library and tests against netstandard2.0 (#10470) * Build ServiceDiscovery library and tests against .NET Framework This enables scenarios to use the library on .NET Framework. This is mostly done by using C# 14 new extension syntax so very little code changes were made but rather the APIs that didn't exist are added into FrameworkExtensions classes to light them up on .NET Core builds. * Add Dependency flow and condition new dependencies to only apply in netfx build. * Fix build break * conditionally inclue targets file * fix merge break * add netstandard2.0 * remove net462 * up the version for 9.0.8 in version.details.xml --------- Co-authored-by: Jose Perez Rodriguez --- ...sions.ServiceDiscovery.Abstractions.csproj | 10 +- ...crosoft.Extensions.ServiceDiscovery.csproj | 14 ++- ...iceDiscoveryHttpClientBuilderExtensions.cs | 12 ++- src/Shared/FxPolyfills/ArgumentException.cs | 28 ++++++ .../FxPolyfills/ArgumentNullException.cs | 24 +++++ .../CallerArgumentExpressionAttribute.cs | 10 ++ .../FxPolyfills/ConcurrentDictionary.cs | 43 +++++++++ .../FxPolyfills/ExceptionDispatchInfo.cs | 18 ++++ src/Shared/FxPolyfills/FxPolyfills.targets | 23 +++++ src/Shared/FxPolyfills/IPEndPoint.cs | 59 ++++++++++++ src/Shared/FxPolyfills/Interlocked.cs | 94 +++++++++++++++++++ src/Shared/FxPolyfills/IsExternalInit.cs | 8 ++ src/Shared/FxPolyfills/KeyValuePair.cs | 24 +++++ .../FxPolyfills/ObjectDisposedException.cs | 20 ++++ src/Shared/FxPolyfills/OperatingSystem.cs | 14 +++ src/Shared/FxPolyfills/String.cs | 12 +++ src/Shared/FxPolyfills/Task.TimeProvider.cs | 15 +++ src/Shared/FxPolyfills/Task.cs | 54 +++++++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 11 ++- .../ServiceEndpointResolverTests.cs | 2 + 20 files changed, 484 insertions(+), 11 deletions(-) create mode 100644 src/Shared/FxPolyfills/ArgumentException.cs create mode 100644 src/Shared/FxPolyfills/ArgumentNullException.cs create mode 100644 src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs create mode 100644 src/Shared/FxPolyfills/ConcurrentDictionary.cs create mode 100644 src/Shared/FxPolyfills/ExceptionDispatchInfo.cs create mode 100644 src/Shared/FxPolyfills/FxPolyfills.targets create mode 100644 src/Shared/FxPolyfills/IPEndPoint.cs create mode 100644 src/Shared/FxPolyfills/Interlocked.cs create mode 100644 src/Shared/FxPolyfills/IsExternalInit.cs create mode 100644 src/Shared/FxPolyfills/KeyValuePair.cs create mode 100644 src/Shared/FxPolyfills/ObjectDisposedException.cs create mode 100644 src/Shared/FxPolyfills/OperatingSystem.cs create mode 100644 src/Shared/FxPolyfills/String.cs create mode 100644 src/Shared/FxPolyfills/Task.TimeProvider.cs create mode 100644 src/Shared/FxPolyfills/Task.cs diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 6f412779689..061e85b44d3 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,9 +1,9 @@ - $(DefaultTargetFramework) + $(DefaultTargetFramework);netstandard2.0 true - true + true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery @@ -19,4 +19,10 @@ + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 59c1c31fee9..72f8f4f9051 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,21 +1,27 @@ - $(DefaultTargetFramework) + netstandard2.0;$(DefaultTargetFramework) true - true + true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) - - + + + + + + + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs index 7d5b94c10c5..d2890ae8c8d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryHttpClientBuilderExtensions.cs @@ -1,12 +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 Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Http; using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Http; +#if NET +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Http; +#endif + namespace Microsoft.Extensions.DependencyInjection; /// @@ -34,13 +37,15 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt return new ResolvingHttpDelegatingHandler(registry, options); }); +#if NET // 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); - +#endif return httpClientBuilder; } +#if NET private static void AddDisableGrpcLoadBalancingFilter(IServiceCollection services, string? name) { // A filter is used because it will always run last. This is important because the disable @@ -86,4 +91,5 @@ public Action Configure(Action throw new ArgumentNullException(paramName); +} diff --git a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000000..6d82b4a25c9 --- /dev/null +++ b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute +{ + public string ParameterName => parameterName; +} diff --git a/src/Shared/FxPolyfills/ConcurrentDictionary.cs b/src/Shared/FxPolyfills/ConcurrentDictionary.cs new file mode 100644 index 00000000000..92e4a2195df --- /dev/null +++ b/src/Shared/FxPolyfills/ConcurrentDictionary.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Concurrent; + +internal static partial class FxPolyfillConcurrentDictionary +{ + extension(ConcurrentDictionary dictionary) + { + public TValue GetOrAdd(TKey key, Func valueFactory) + { + if (dictionary.TryGetValue(key, out var existing)) + { + return existing; + } + + return dictionary.GetOrAdd(key, valueFactory(key)); + } + + public TValue GetOrAdd(TKey key, Func valueFactory, TState state) + { + if (dictionary.TryGetValue(key, out var existing)) + { + return existing; + } + + return dictionary.GetOrAdd(key, valueFactory(key, state)); + } + + public void TryRemove(TKey key) + { + dictionary.TryRemove(key, out _); + } + + public void TryRemove(KeyValuePair pair) + { + if (dictionary.TryRemove(pair.Key, out var existing) && !EqualityComparer.Default.Equals(existing, pair.Value)) + { + dictionary.TryAdd(pair.Key, pair.Value); + } + } + } +} diff --git a/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs b/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs new file mode 100644 index 00000000000..81cee7cba9a --- /dev/null +++ b/src/Shared/FxPolyfills/ExceptionDispatchInfo.cs @@ -0,0 +1,18 @@ +// 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 System.Runtime.ExceptionServices; + +internal static partial class FxPolyfillExceptionDispatchInfo +{ + extension(ExceptionDispatchInfo) + { + [DoesNotReturn] + public static void Throw(Exception ex) + { + ExceptionDispatchInfo.Capture(ex).Throw(); + } + } +} diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets new file mode 100644 index 00000000000..ea9db526b69 --- /dev/null +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -0,0 +1,23 @@ + + + $(MSBuildThisFileDirectory) + + + + + + + + + + + + + + + + diff --git a/src/Shared/FxPolyfills/IPEndPoint.cs b/src/Shared/FxPolyfills/IPEndPoint.cs new file mode 100644 index 00000000000..8571b675bb5 --- /dev/null +++ b/src/Shared/FxPolyfills/IPEndPoint.cs @@ -0,0 +1,59 @@ +// 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; + +namespace System.Net; + +internal static partial class FxPolyfillIPEndPoint +{ + extension(IPEndPoint) + { + public static IPEndPoint Parse(string endpoint) + { + if (TryParse(endpoint.AsSpan(), out var result)) + { + return result; + } + + throw new FormatException("The endpoint format is invalid."); + } + + public static bool TryParse(ReadOnlySpan s, out IPEndPoint? result) + { + const int MaxPort = 0x0000FFFF; + + int addressLength = s.Length; // If there's no port then send the entire string to the address parser + int lastColonPos = s.LastIndexOf(':'); + + // Look to see if this is an IPv6 address with a port. + if (lastColonPos > 0) + { + if (s[lastColonPos - 1] == ']') + { + addressLength = lastColonPos; + } + // Look to see if this is IPv4 with a port (IPv6 will have another colon) + else if (s.Slice(0, lastColonPos).LastIndexOf(':') == -1) + { + addressLength = lastColonPos; + } + } + + if (IPAddress.TryParse(s.Slice(0, addressLength).ToString(), out IPAddress? address)) + { + uint port = 0; + if (addressLength == s.Length || + (uint.TryParse(s.Slice(addressLength + 1).ToString(), NumberStyles.None, CultureInfo.InvariantCulture, out port) && port <= MaxPort)) + + { + result = new IPEndPoint(address, (int)port); + return true; + } + } + + result = null; + return false; + } + } +} diff --git a/src/Shared/FxPolyfills/Interlocked.cs b/src/Shared/FxPolyfills/Interlocked.cs new file mode 100644 index 00000000000..6177e411c35 --- /dev/null +++ b/src/Shared/FxPolyfills/Interlocked.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Threading; + +internal static partial class FxPolyfillInterlocked +{ + extension(Interlocked) + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Decrement(ref uint location) => + (uint)Interlocked.Add(ref Unsafe.As(ref location), -1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Decrement(ref ulong location) => + (ulong)Interlocked.Add(ref Unsafe.As(ref location), -1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Increment(ref uint location) => + Add(ref location, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Increment(ref ulong location) => + Add(ref location, 1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Add(ref uint location1, uint value) => + (uint)Interlocked.Add(ref Unsafe.As(ref location1), (int)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Add(ref ulong location1, ulong value) => + (ulong)Interlocked.Add(ref Unsafe.As(ref location1), (long)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long Or(ref long location1, long value) + { + long current = location1; + while (true) + { + long newValue = current | value; + long oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong And(ref ulong location1, ulong value) => + (ulong)Interlocked.And(ref Unsafe.As(ref location1), (long)value); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint And(ref uint location1, uint value) => + (uint)Interlocked.And(ref Unsafe.As(ref location1), (int)value); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int And(ref int location1, int value) + { + int current = location1; + while (true) + { + int newValue = current & value; + int oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long And(ref long location1, long value) + { + long current = location1; + while (true) + { + long newValue = current & value; + long oldValue = Interlocked.CompareExchange(ref location1, newValue, current); + if (oldValue == current) + { + return oldValue; + } + current = oldValue; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong Or(ref ulong location1, ulong value) => + (ulong)Or(ref Unsafe.As(ref location1), (long)value); + } +} diff --git a/src/Shared/FxPolyfills/IsExternalInit.cs b/src/Shared/FxPolyfills/IsExternalInit.cs new file mode 100644 index 00000000000..f2bac777b13 --- /dev/null +++ b/src/Shared/FxPolyfills/IsExternalInit.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices; + +internal static class IsExternalInit +{ +} diff --git a/src/Shared/FxPolyfills/KeyValuePair.cs b/src/Shared/FxPolyfills/KeyValuePair.cs new file mode 100644 index 00000000000..64c79606bf4 --- /dev/null +++ b/src/Shared/FxPolyfills/KeyValuePair.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Collections.Generic; + +internal static partial class FxPolyfillKeyValuePair +{ + extension(KeyValuePair pair) + { + public void Deconstruct(out TKey key, out TValue value) + { + key = pair.Key; + value = pair.Value; + } + } +} + +internal static class KeyValuePair +{ + public static KeyValuePair Create(TKey key, TValue value) + { + return new KeyValuePair(key, value); + } +} diff --git a/src/Shared/FxPolyfills/ObjectDisposedException.cs b/src/Shared/FxPolyfills/ObjectDisposedException.cs new file mode 100644 index 00000000000..85ec090dd6c --- /dev/null +++ b/src/Shared/FxPolyfills/ObjectDisposedException.cs @@ -0,0 +1,20 @@ +// 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 System; + +internal static partial class FxPolyfillObjectDisposedException +{ + extension(ObjectDisposedException) + { + public static void ThrowIf([DoesNotReturnIf(true)] bool condition, object instance) + { + if (condition) + { + throw new ObjectDisposedException(instance?.GetType().FullName); + } + } + } +} diff --git a/src/Shared/FxPolyfills/OperatingSystem.cs b/src/Shared/FxPolyfills/OperatingSystem.cs new file mode 100644 index 00000000000..4c88e7909aa --- /dev/null +++ b/src/Shared/FxPolyfills/OperatingSystem.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +internal static class FrameworkExtensions +{ + extension(OperatingSystem) + { + public static bool IsLinux() => false; + public static bool IsWindows() => true; + public static bool IsMacOS() => false; + } +} diff --git a/src/Shared/FxPolyfills/String.cs b/src/Shared/FxPolyfills/String.cs new file mode 100644 index 00000000000..92df065be7e --- /dev/null +++ b/src/Shared/FxPolyfills/String.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System; + +internal static partial class FxPolyfillString +{ + extension(string s) + { + public bool StartsWith(char c) => s is [{ } first, ..] && first == c; + } +} diff --git a/src/Shared/FxPolyfills/Task.TimeProvider.cs b/src/Shared/FxPolyfills/Task.TimeProvider.cs new file mode 100644 index 00000000000..7e3a9c85bf0 --- /dev/null +++ b/src/Shared/FxPolyfills/Task.TimeProvider.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks; + +internal static partial class FxPolyfillTask +{ + extension(Task task) + { + public Task WaitAsync(CancellationToken token) + { + return task.WaitAsync(Timeout.InfiniteTimeSpan, TimeProvider.System, token); + } + } +} diff --git a/src/Shared/FxPolyfills/Task.cs b/src/Shared/FxPolyfills/Task.cs new file mode 100644 index 00000000000..0035cde4b6f --- /dev/null +++ b/src/Shared/FxPolyfills/Task.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Threading.Tasks; + +internal enum ConfigureAwaitOptions +{ + None, + ContinueOnCapturedContext, + ForceYielding, + SuppressThrowing, +} + +internal static partial class FxPolyfillTask +{ + extension(Task task) + { + public async Task ConfigureAwait(ConfigureAwaitOptions options) + { + if (options == ConfigureAwaitOptions.None) + { + await task.ConfigureAwait(false); + } + else if (options == ConfigureAwaitOptions.ContinueOnCapturedContext) + { + await task.ConfigureAwait(true); + } + else if (options == ConfigureAwaitOptions.ForceYielding) + { + await Task.Yield(); + await task.ConfigureAwait(false); + } + else if (options == ConfigureAwaitOptions.SuppressThrowing) + { + try + { + await task.ConfigureAwait(false); + } + catch + { + } + } + else + { + throw new InvalidOperationException(); + } + } + } +} + +internal sealed class TaskCompletionSource(TaskCreationOptions options) : TaskCompletionSource(options) +{ + public void SetResult() => SetResult(true); +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 269081ae5d7..20147d5d465 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,11 +1,19 @@ - $(DefaultTargetFramework) + $(DefaultTargetFramework) + $(TargetFrameworks);net472 enable enable + + + + + + + @@ -15,5 +23,4 @@ - diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs index 0e08c07271e..c91f07c9300 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/ServiceEndpointResolverTests.cs @@ -66,7 +66,9 @@ public async Task AddServiceDiscovery_NoProviders_Throws() private sealed class FakeEndpointResolverProvider(Func createResolverDelegate) : IServiceEndpointProviderFactory { +#pragma warning disable CS0436 // Type conflicts with imported type public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? resolver) +#pragma warning restore CS0436 // Type conflicts with imported type { bool result; (result, resolver) = createResolverDelegate(query); From d1008fbbc6aa99d75cdff006bf72d622f75fba88 Mon Sep 17 00:00:00 2001 From: Taylor Southwick Date: Tue, 2 Sep 2025 10:04:18 -0700 Subject: [PATCH 74/77] Add net462 target to ServiceDiscovery (#11114) An add on from #10470 that added support for netstandard2.0, this adds an explicit net462 target which is part of the recommendation for multi-targeted libraries. --- ...rosoft.Extensions.ServiceDiscovery.Abstractions.csproj | 8 ++++---- .../Microsoft.Extensions.ServiceDiscovery.csproj | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 061e85b44d3..c32fb4c87e5 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,9 +1,9 @@ - $(DefaultTargetFramework);netstandard2.0 + netstandard2.0;net462;$(DefaultTargetFramework) true - true + true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery @@ -19,10 +19,10 @@ - + - + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 72f8f4f9051..2556df195e6 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,9 +1,9 @@ - netstandard2.0;$(DefaultTargetFramework) + netstandard2.0;net462;$(DefaultTargetFramework) true - true + true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) @@ -18,10 +18,10 @@ - + - + From 0df2dc11014e3d60ae27c32267be398cb807d93e Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 12:25:48 -0500 Subject: [PATCH 75/77] Get ServiceDiscovery libraries and tests building clean in dotnet/extensions. For now, disable analyzers in the projects to get them building. --- eng/Version.Details.xml | 12 ++ eng/Versions.props | 5 + eng/packages/General-LTS.props | 3 + eng/packages/General-net9.props | 3 + eng/packages/General.props | 3 + ...sions.ServiceDiscovery.Abstractions.csproj | 12 +- ...xtensions.ServiceDiscovery.Abstractions.cs | 71 --------- ...oft.Extensions.ServiceDiscovery.Dns.csproj | 7 +- ...crosoft.Extensions.ServiceDiscovery.Dns.cs | 52 ------- ...ft.Extensions.ServiceDiscovery.Yarp.csproj | 6 +- ...rosoft.Extensions.ServiceDiscovery.Yarp.cs | 19 --- ...crosoft.Extensions.ServiceDiscovery.csproj | 9 +- .../Microsoft.Extensions.ServiceDiscovery.cs | 68 -------- .../CallerArgumentExpressionAttribute.cs | 10 -- src/Shared/FxPolyfills/FxPolyfills.targets | 2 + src/Shared/FxPolyfills/IsExternalInit.cs | 8 - ....ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 5 +- ...tensions.ServiceDiscovery.Dns.Tests.csproj | 14 +- .../Resolver/CancellationTests.cs | 2 +- .../Resolver/LoopbackDnsTestBase.cs | 9 +- .../Resolver/ResolveAddressesTests.cs | 4 +- .../Resolver/ResolveServiceTests.cs | 2 +- .../Resolver/RetryTests.cs | 2 +- .../Resolver/TcpFailoverTests.cs | 2 +- .../XunitLoggerFactoryExtensions.cs | 145 ++++++++++++++++++ ...t.Extensions.ServiceDiscovery.Tests.csproj | 10 +- ...ensions.ServiceDiscovery.Yarp.Tests.csproj | 13 +- 27 files changed, 227 insertions(+), 271 deletions(-) delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs delete mode 100644 src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs delete mode 100644 src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs delete mode 100644 src/Shared/FxPolyfills/IsExternalInit.cs create mode 100644 test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e4f5ea11353..3b5a0c53b49 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -4,6 +4,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 893c2ebbd49952ca49e93298148af2d95a61a0a4 + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 @@ -80,6 +84,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 + + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime + 893c2ebbd49952ca49e93298148af2d95a61a0a4 + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 893c2ebbd49952ca49e93298148af2d95a61a0a4 @@ -180,6 +188,10 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore ff66c263be7ed395794bdaf616322977b8ec897c + + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore + ff66c263be7ed395794bdaf616322977b8ec897c + https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore ff66c263be7ed395794bdaf616322977b8ec897c diff --git a/eng/Versions.props b/eng/Versions.props index 91a09df773f..22d61962791 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -34,6 +34,7 @@ 9.0.9 + 9.0.9 9.0.9 9.0.9 9.0.9 @@ -53,6 +54,7 @@ 9.0.9 9.0.9 9.0.9 + 9.0.9 9.0.9 9.0.9 9.0.9 @@ -78,6 +80,7 @@ 9.0.9 9.0.9 9.0.9 + 9.0.9 9.0.9 9.0.9 @@ -107,6 +110,7 @@ 8.0.1 8.0.0 8.0.2 + 8.0.0 8.0.20 8.0.20 8.0.0 @@ -132,6 +136,7 @@ 8.0.20 8.0.20 8.0.20 + 8.0.20 8.0.20 8.0.20 diff --git a/eng/packages/General-LTS.props b/eng/packages/General-LTS.props index 884d874c5e1..e5e06d632de 100644 --- a/eng/packages/General-LTS.props +++ b/eng/packages/General-LTS.props @@ -4,6 +4,7 @@ of the framework, we should use the following LTS versions instead --> + @@ -17,6 +18,7 @@ + @@ -28,6 +30,7 @@ + diff --git a/eng/packages/General-net9.props b/eng/packages/General-net9.props index 341f69458a8..e3ff1198cec 100644 --- a/eng/packages/General-net9.props +++ b/eng/packages/General-net9.props @@ -4,6 +4,7 @@ of the framework, the following versions should be used. --> + @@ -17,6 +18,7 @@ + @@ -28,6 +30,7 @@ + diff --git a/eng/packages/General.props b/eng/packages/General.props index 5be4031ad4d..7a5bd0d46a0 100644 --- a/eng/packages/General.props +++ b/eng/packages/General.props @@ -5,6 +5,7 @@ + @@ -21,6 +22,7 @@ + @@ -33,6 +35,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index c32fb4c87e5..494e1cfbfbb 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -1,12 +1,14 @@ - + - netstandard2.0;net462;$(DefaultTargetFramework) + $(TargetFrameworks);netstandard2.0 true - true Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery + + $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 + enable @@ -22,7 +24,7 @@ - - + + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs deleted file mode 100644 index a7ed4ec5404..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/api/Microsoft.Extensions.ServiceDiscovery.Abstractions.cs +++ /dev/null @@ -1,71 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.ServiceDiscovery -{ - public partial interface IHostNameFeature - { - string HostName { get; } - } - - public partial interface IServiceEndpointBuilder - { - System.Collections.Generic.IList Endpoints { get; } - - AspNetCore.Http.Features.IFeatureCollection Features { get; } - - void AddChangeToken(Primitives.IChangeToken changeToken); - } - - public partial interface IServiceEndpointProvider : System.IAsyncDisposable - { - System.Threading.Tasks.ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, System.Threading.CancellationToken cancellationToken); - } - - public partial interface IServiceEndpointProviderFactory - { - bool TryCreateProvider(ServiceEndpointQuery query, out IServiceEndpointProvider? provider); - } - - public abstract partial class ServiceEndpoint - { - public abstract System.Net.EndPoint EndPoint { get; } - public abstract AspNetCore.Http.Features.IFeatureCollection Features { get; } - - public static ServiceEndpoint Create(System.Net.EndPoint endPoint, AspNetCore.Http.Features.IFeatureCollection? features = null) { throw null; } - } - - public sealed partial class ServiceEndpointQuery - { - internal ServiceEndpointQuery() { } - - public string? EndpointName { get { throw null; } } - - public System.Collections.Generic.IReadOnlyList IncludedSchemes { get { throw null; } } - - public string ServiceName { get { throw null; } } - - public override string? ToString() { throw null; } - - public static bool TryParse(string input, out ServiceEndpointQuery? query) { throw null; } - } - - [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")] - public sealed partial class ServiceEndpointSource - { - public ServiceEndpointSource(System.Collections.Generic.List? endpoints, Primitives.IChangeToken changeToken, AspNetCore.Http.Features.IFeatureCollection features) { } - - public Primitives.IChangeToken ChangeToken { get { throw null; } } - - public System.Collections.Generic.IReadOnlyList Endpoints { get { throw null; } } - - public AspNetCore.Http.Features.IFeatureCollection Features { get { throw null; } } - - public override string ToString() { throw null; } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index 6d8bbb47842..f1a59b08dac 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -1,11 +1,14 @@ - $(DefaultTargetFramework) + $(NetCoreTargetFrameworks) true - true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. $(DefaultDotnetIconFullPath) + + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1515;SA1600;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable + false diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs deleted file mode 100644 index 15f99b179ec..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/api/Microsoft.Extensions.ServiceDiscovery.Dns.cs +++ /dev/null @@ -1,52 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.Hosting -{ - public static partial class ServiceDiscoveryDnsServiceCollectionExtensions - { - public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services, System.Action configureOptions) { throw null; } - - public static DependencyInjection.IServiceCollection AddDnsSrvServiceEndpointProvider(this DependencyInjection.IServiceCollection services) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery.Dns -{ - public partial class DnsServiceEndpointProviderOptions - { - public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } - - public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } - - public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } - - public double RetryBackOffFactor { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } - - public partial class DnsSrvServiceEndpointProviderOptions - { - public System.TimeSpan DefaultRefreshPeriod { get { throw null; } set { } } - - public System.TimeSpan MaxRetryPeriod { get { throw null; } set { } } - - public System.TimeSpan MinRetryPeriod { get { throw null; } set { } } - - public string? QuerySuffix { get { throw null; } set { } } - - public double RetryBackOffFactor { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 74870a87668..16da6587759 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -1,13 +1,15 @@ - $(DefaultTargetFramework) + $(NetCoreTargetFrameworks) enable enable true - true Provides extensions for service discovery for the YARP reverse proxy. $(DefaultDotnetIconFullPath) + + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S2692;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1414;SA1515;SA1600;SA1615;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs deleted file mode 100644 index fc608f86a92..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/api/Microsoft.Extensions.ServiceDiscovery.Yarp.cs +++ /dev/null @@ -1,19 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection -{ - public static partial class ServiceDiscoveryReverseProxyServiceCollectionExtensions - { - public static IServiceCollection AddHttpForwarderWithServiceDiscovery(this IServiceCollection services) { throw null; } - - public static IReverseProxyBuilder AddServiceDiscoveryDestinationResolver(this IReverseProxyBuilder builder) { throw null; } - - public static IServiceCollection AddServiceDiscoveryForwarderFactory(this IServiceCollection services) { throw null; } - } -} \ No newline at end of file diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index 2556df195e6..d501e3e22ad 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -1,11 +1,14 @@ - netstandard2.0;net462;$(DefaultTargetFramework) + $(TargetFrameworks);netstandard2.0 true - true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) + + $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 + enable + false @@ -22,6 +25,6 @@ - + diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs deleted file mode 100644 index a6ba654085e..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/api/Microsoft.Extensions.ServiceDiscovery.cs +++ /dev/null @@ -1,68 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ -namespace Microsoft.Extensions.DependencyInjection -{ - public static partial class ServiceDiscoveryHttpClientBuilderExtensions - { - public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder httpClientBuilder) { throw null; } - } - - public static partial class ServiceDiscoveryServiceCollectionExtensions - { - public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddConfigurationServiceEndpointProvider(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddPassThroughServiceEndpointProvider(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddServiceDiscovery(this IServiceCollection services) { throw null; } - - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services, System.Action configureOptions) { throw null; } - - public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection services) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery -{ - public sealed partial class ConfigurationServiceEndpointProviderOptions - { - public string SectionName { get { throw null; } set { } } - - public System.Func ShouldApplyHostNameMetadata { get { throw null; } set { } } - } - - public sealed partial class ServiceDiscoveryOptions - { - public bool AllowAllSchemes { get { throw null; } set { } } - - public System.Collections.Generic.IList AllowedSchemes { get { throw null; } set { } } - - public System.TimeSpan RefreshPeriod { get { throw null; } set { } } - } - - public sealed partial class ServiceEndpointResolver : System.IAsyncDisposable - { - internal ServiceEndpointResolver() { } - - public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } - - public System.Threading.Tasks.ValueTask GetEndpointsAsync(string serviceName, System.Threading.CancellationToken cancellationToken) { throw null; } - } -} - -namespace Microsoft.Extensions.ServiceDiscovery.Http -{ - public partial interface IServiceDiscoveryHttpMessageHandlerFactory - { - System.Net.Http.HttpMessageHandler CreateHandler(System.Net.Http.HttpMessageHandler handler); - } -} \ No newline at end of file diff --git a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs b/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs deleted file mode 100644 index 6d82b4a25c9..00000000000 --- a/src/Shared/FxPolyfills/CallerArgumentExpressionAttribute.cs +++ /dev/null @@ -1,10 +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 System.Runtime.CompilerServices; - -[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] -internal sealed class CallerArgumentExpressionAttribute(string parameterName) : Attribute -{ - public string ParameterName => parameterName; -} diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets index ea9db526b69..cc03ee52970 100644 --- a/src/Shared/FxPolyfills/FxPolyfills.targets +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -1,6 +1,8 @@ $(MSBuildThisFileDirectory) + + $(NoWarn);CS8763;CS8777;CS8603;CA1031;IDE0058;S108;S2166;S2302;S2333;S2486;S3400;SA1402;SA1509;SA1515;SA1649;EA0014;LA0001;VSTHRD003 diff --git a/src/Shared/FxPolyfills/IsExternalInit.cs b/src/Shared/FxPolyfills/IsExternalInit.cs deleted file mode 100644 index f2bac777b13..00000000000 --- a/src/Shared/FxPolyfills/IsExternalInit.cs +++ /dev/null @@ -1,8 +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 System.Runtime.CompilerServices; - -internal static class IsExternalInit -{ -} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj index 6572c27a1fa..1e4a55d35bb 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -1,10 +1,11 @@  - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable Exe + $(NoWarn);IDE0040;IDE0061;IDE1006;S5034;SA1400;VSTHRD002 @@ -12,7 +13,7 @@ - + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index f6202d17d37..45603e5fc5c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -1,15 +1,14 @@ - + - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);IDE0004;IDE0017;IDE0040;IDE0055;IDE1006;CA1012;CA1031;CA1063;CA1816;CA2000;S103;S107;S1067;S1121;S1128;S1135;S1144;S1186;S2148;S3442;S3459;S4136;SA1106;SA1127;SA1204;SA1208;SA1210;SA1128;SA1316;SA1400;SA1402;SA1407;SA1414;SA1500;SA1513;SA1515;VSTHRD003 - - @@ -17,9 +16,10 @@ - - - + + + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs index 8c646ac18ee..786882afc1d 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/CancellationTests.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs index f76621db93c..14abd659029 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -1,10 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; -using Microsoft.Extensions.Time.Testing; -using Microsoft.Extensions.Logging; +using System.Globalization; +using System.Text; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery.Dns.Tests; +using Microsoft.Extensions.Time.Testing; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs index b87e1362f3d..c2d033ecdae 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveAddressesTests.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; @@ -304,4 +304,4 @@ public async Task Resolve_HeaderMismatch_Ignores() Assert.Equal(address, result.Address); } -} \ No newline at end of file +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs index e1cd1df2959..82ca3175789 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolveServiceTests.cs @@ -1,8 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs index 800905d1ac5..49985846570 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -3,7 +3,7 @@ using System.Net; using System.Net.Sockets; -using Xunit; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs index 40841e3d11a..cbdb5e282e9 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; using System.Net; using System.Net.Sockets; +using Xunit.Abstractions; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs new file mode 100644 index 00000000000..6667688f16e --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/XunitLoggerFactoryExtensions.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +internal static class XunitLoggerFactoryExtensions +{ + public static ILoggingBuilder AddXunit(this ILoggingBuilder builder, ITestOutputHelper output) + { + builder.Services.AddSingleton(new XunitLoggerProvider(output)); + return builder; + } + + public static IServiceCollection AddXunitLogging(this IServiceCollection services, ITestOutputHelper output) => + services.AddLogging(b => b.AddXunit(output)); +} + +internal class XunitLoggerProvider : ILoggerProvider +{ + private readonly ITestOutputHelper _output; + private readonly LogLevel _minLevel; + private readonly DateTimeOffset? _logStart; + + public XunitLoggerProvider(ITestOutputHelper output) + : this(output, LogLevel.Trace) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel) + : this(output, minLevel, null) + { + } + + public XunitLoggerProvider(ITestOutputHelper output, LogLevel minLevel, DateTimeOffset? logStart) + { + _output = output; + _minLevel = minLevel; + _logStart = logStart; + } + + public ILogger CreateLogger(string categoryName) + { + return new XunitLogger(_output, categoryName, _minLevel, _logStart); + } + + public void Dispose() + { + } +} + +internal class XunitLogger : ILogger +{ + private static readonly string[] s_newLineChars = new[] { Environment.NewLine }; + private readonly string _category; + private readonly LogLevel _minLogLevel; + private readonly ITestOutputHelper _output; + private readonly DateTimeOffset? _logStart; + + public XunitLogger(ITestOutputHelper output, string category, LogLevel minLogLevel, DateTimeOffset? logStart) + { + _minLogLevel = minLogLevel; + _category = category; + _output = output; + _logStart = logStart; + } + + public void Log( + LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + // Buffer the message into a single string in order to avoid shearing the message when running across multiple threads. + var messageBuilder = new StringBuilder(); + + var timestamp = _logStart.HasValue ? + $"{(DateTimeOffset.UtcNow - _logStart.Value).TotalSeconds.ToString("N3", CultureInfo.InvariantCulture)}s" : + DateTimeOffset.UtcNow.ToString("s", CultureInfo.InvariantCulture); + + var firstLinePrefix = $"| [{timestamp}] {_category} {logLevel}: "; + var lines = formatter(state, exception).Split(s_newLineChars, StringSplitOptions.RemoveEmptyEntries); + messageBuilder.AppendLine(firstLinePrefix + lines.FirstOrDefault() ?? string.Empty); + + var additionalLinePrefix = "|" + new string(' ', firstLinePrefix.Length - 1); + foreach (var line in lines.Skip(1)) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + + if (exception != null) + { + lines = exception.ToString().Split(s_newLineChars, StringSplitOptions.RemoveEmptyEntries); + additionalLinePrefix = "| "; + foreach (var line in lines) + { + messageBuilder.AppendLine(additionalLinePrefix + line); + } + } + + // Remove the last line-break, because ITestOutputHelper only has WriteLine. + var message = messageBuilder.ToString(); + if (message.EndsWith(Environment.NewLine, StringComparison.Ordinal)) + { + message = message.Substring(0, message.Length - Environment.NewLine.Length); + } + + try + { + _output.WriteLine(message); + } + catch (Exception) + { + // We could fail because we're on a background thread and our captured ITestOutputHelper is + // busted (if the test "completed" before the background thread fired). + // So, ignore this. There isn't really anything we can do but hope the + // caller has additional loggers registered + } + } + + public bool IsEnabled(LogLevel logLevel) + => logLevel >= _minLogLevel; + + public IDisposable BeginScope(TState state) where TState : notnull + => new NullScope(); + + private sealed class NullScope : IDisposable + { + public void Dispose() + { + } + } +} + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 20147d5d465..1a9f8aaae8c 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -1,13 +1,12 @@ - $(DefaultTargetFramework) - $(TargetFrameworks);net472 enable enable + $(NoWarn);IDE0004;IDE0040;IDE0055;IDE1006;CA2000;S1121;S1128;SA1316;SA1500;SA1513 - + @@ -15,12 +14,11 @@ - - - + + diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index 296a3dcd861..c097e0f2503 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -1,23 +1,22 @@ - $(DefaultTargetFramework) + $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);CA2000;S103;S1144;S3459;S4136;SA1208;SA1210;VSTHRD003 - - - - - - + + + + From 0034beec76acb6f99b36ce26f07cdaec38c33f77 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 14:49:59 -0500 Subject: [PATCH 76/77] Remove FxPolyfills from the Shared.csproj --- src/Shared/Shared.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Shared/Shared.csproj b/src/Shared/Shared.csproj index d25c011a05f..ecffd480a44 100644 --- a/src/Shared/Shared.csproj +++ b/src/Shared/Shared.csproj @@ -26,6 +26,10 @@ 85 + + + + From 675c83e1a7b47e19939d80e30ac5bb1478c1497a Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 30 Sep 2025 15:57:32 -0500 Subject: [PATCH 77/77] PR Feedback --- .../Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj | 2 +- .../ServiceEndpointQuery.cs | 2 +- .../DnsSrvServiceEndpointProviderFactory.cs | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Dns.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.Yarp.csproj | 2 +- .../Microsoft.Extensions.ServiceDiscovery.csproj | 2 +- src/Shared/FxPolyfills/FxPolyfills.targets | 2 +- ...crosoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Tests.csproj | 1 + .../Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj | 1 + 11 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj index 494e1cfbfbb..b3a9f892419 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/Microsoft.Extensions.ServiceDiscovery.Abstractions.csproj @@ -6,7 +6,7 @@ Provides abstractions for service discovery. Interfaces defined in this package are implemented in Microsoft.Extensions.ServiceDiscovery and other service discovery packages. $(DefaultDotnetIconFullPath) Microsoft.Extensions.ServiceDiscovery - + $(NoWarn);S1144;CA1002;S2365;SA1642;IDE0040;CA1307;EA0009;LA0003 enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs index 20a17d0878f..36fca0893cc 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpointQuery.cs @@ -38,7 +38,7 @@ public static bool TryParse(string input, [NotNullWhen(true)] out ServiceEndpoin ArgumentException.ThrowIfNullOrEmpty(input); bool hasScheme; - if (!input.Contains("://", StringComparison.InvariantCulture) + if (!input.Contains("://", StringComparison.Ordinal) && Uri.TryCreate($"fakescheme://{input}", default, out var uri)) { hasScheme = false; diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 085ee30123b..57820560a63 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -95,7 +95,7 @@ public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] ou var lines = File.ReadAllLines(s_resolveConfPath); foreach (var line in lines) { - if (!line.StartsWith("search ")) + if (!line.StartsWith("search ", StringComparison.Ordinal)) { continue; } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj index f1a59b08dac..890f8daab3e 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Microsoft.Extensions.ServiceDiscovery.Dns.csproj @@ -5,7 +5,7 @@ true Provides extensions to HttpClient to resolve well-known hostnames to concrete endpoints based on DNS records. Useful for service resolution in orchestrators such as Kubernetes. $(DefaultDotnetIconFullPath) - + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1515;SA1600;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj index 16da6587759..e990866bd16 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp/Microsoft.Extensions.ServiceDiscovery.Yarp.csproj @@ -7,7 +7,7 @@ true Provides extensions for service discovery for the YARP reverse proxy. $(DefaultDotnetIconFullPath) - + $(NoWarn);IDE0018;IDE0025;IDE0032;IDE0040;IDE0058;IDE0250;IDE0251;IDE1006;CA1304;CA1307;CA1309;CA1310;CA1849;CA2000;CA2213;CA2217;S125;S1135;S1226;S2344;S2692;S3626;S4022;SA1108;SA1120;SA1128;SA1129;SA1204;SA1205;SA1214;SA1400;SA1405;SA1408;SA1414;SA1515;SA1600;SA1615;SA1629;SA1642;SA1649;EA0001;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj index d501e3e22ad..8ff69baf576 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Microsoft.Extensions.ServiceDiscovery.csproj @@ -5,7 +5,7 @@ true Provides extensions to HttpClient that enable service discovery based on configuration. $(DefaultDotnetIconFullPath) - + $(NoWarn);CS8600;CS8602;CS8604;IDE0040;IDE0055;IDE0058;IDE1006;CA1307;CA1310;CA1849;CA2007;CA2213;SA1204;SA1128;SA1205;SA1405;SA1612;SA1623;SA1625;SA1642;S1144;S1449;S2302;S2692;S3872;S4457;EA0000;EA0009;EA0014;LA0001;LA0003;LA0008;VSTHRD200 enable false diff --git a/src/Shared/FxPolyfills/FxPolyfills.targets b/src/Shared/FxPolyfills/FxPolyfills.targets index cc03ee52970..ca38f9ab986 100644 --- a/src/Shared/FxPolyfills/FxPolyfills.targets +++ b/src/Shared/FxPolyfills/FxPolyfills.targets @@ -1,7 +1,7 @@ $(MSBuildThisFileDirectory) - + $(NoWarn);CS8763;CS8777;CS8603;CA1031;IDE0058;S108;S2166;S2302;S2333;S2486;S3400;SA1402;SA1509;SA1515;SA1649;EA0014;LA0001;VSTHRD003 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj index 1e4a55d35bb..c291dff12c8 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing.csproj @@ -5,6 +5,7 @@ enable enable Exe + $(NoWarn);IDE0040;IDE0061;IDE1006;S5034;SA1400;VSTHRD002 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj index 45603e5fc5c..4911d854007 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);IDE0004;IDE0017;IDE0040;IDE0055;IDE1006;CA1012;CA1031;CA1063;CA1816;CA2000;S103;S107;S1067;S1121;S1128;S1135;S1144;S1186;S2148;S3442;S3459;S4136;SA1106;SA1127;SA1204;SA1208;SA1210;SA1128;SA1316;SA1400;SA1402;SA1407;SA1414;SA1500;SA1513;SA1515;VSTHRD003 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj index 1a9f8aaae8c..6a39ff1b9af 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Tests/Microsoft.Extensions.ServiceDiscovery.Tests.csproj @@ -3,6 +3,7 @@ enable enable + $(NoWarn);IDE0004;IDE0040;IDE0055;IDE1006;CA2000;S1121;S1128;SA1316;SA1500;SA1513 diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj index c097e0f2503..00211519268 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests.csproj @@ -4,6 +4,7 @@ $(TestNetCoreTargetFrameworks) enable enable + $(NoWarn);CA2000;S103;S1144;S3459;S4136;SA1208;SA1210;VSTHRD003