From 5a4f16ef9f3276c4e119f45f420027a6e2d54286 Mon Sep 17 00:00:00 2001
From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com>
Date: Wed, 22 Oct 2025 14:24:03 +0200
Subject: [PATCH 1/2] Expose building blocks for external service discovery
implementations
---
.../ServiceEndpoint.cs | 47 +++++++++++++++++++
.../UriEndPoint.cs | 17 +++++--
.../ConfigurationServiceEndpointProvider.cs | 43 ++---------------
.../ServiceDiscoveryOptions.cs | 33 ++++++++-----
4 files changed, 84 insertions(+), 56 deletions(-)
rename src/Libraries/{Microsoft.Extensions.ServiceDiscovery => Microsoft.Extensions.ServiceDiscovery.Abstractions}/UriEndPoint.cs (63%)
diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs
index 33e0eff4d69..541390c382e 100644
--- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs
+++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.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.Diagnostics.CodeAnalysis;
using System.Net;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.ServiceDiscovery.Internal;
@@ -34,4 +35,50 @@ public static ServiceEndpoint Create(EndPoint endPoint, IFeatureCollection? feat
return new ServiceEndpointImpl(endPoint, features);
}
+
+ ///
+ /// Tries to convert a specified string representation to its equivalent,
+ /// and returns a value that indicates whether the conversion succeeded.
+ ///
+ /// A string that consists of an IP address or hostname, optionally followed by a colon and port number, or a URI.
+ /// When this method returns, contains the equivalent if the conversion succeeded; otherwise,
+ /// . This parameter is passed uninitialized; any value originally supplied will be overwritten.
+ /// if the string was successfully parsed into a ; otherwise, .
+ public static bool TryParse([NotNullWhen(true)] string? value,
+ [NotNullWhen(true)] out ServiceEndpoint? serviceEndpoint)
+ {
+ EndPoint? endPoint = TryParseEndPoint(value);
+
+ if (endPoint != null)
+ {
+ serviceEndpoint = Create(endPoint);
+ return true;
+ }
+ else
+ {
+ serviceEndpoint = null;
+ return false;
+ }
+ }
+
+ private static EndPoint? TryParseEndPoint(string? value)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ if (value.IndexOf("://", StringComparison.Ordinal) < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri))
+ {
+ var port = uri.Port > 0 ? uri.Port : 0;
+ return IPAddress.TryParse(uri.Host, out var ip)
+ ? new IPEndPoint(ip, port)
+ : new DnsEndPoint(uri.Host, port);
+ }
+
+ if (Uri.TryCreate(value, default, out uri))
+ {
+ return new UriEndPoint(uri);
+ }
+ }
+
+ return null;
+ }
}
diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs
similarity index 63%
rename from src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs
rename to src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs
index 6b5b07d199e..d7bb1ab040e 100644
--- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/UriEndPoint.cs
+++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/UriEndPoint.cs
@@ -8,13 +8,22 @@ namespace Microsoft.Extensions.ServiceDiscovery;
///
/// An endpoint represented by a .
///
-/// The .
-internal sealed class UriEndPoint(Uri uri) : EndPoint
+public class UriEndPoint : EndPoint
{
+ ///
+ /// Creates a new .
+ ///
+ /// The .
+ public UriEndPoint(Uri uri)
+ {
+ ArgumentNullException.ThrowIfNull(uri);
+ Uri = uri;
+ }
+
///
/// Gets the associated with this endpoint.
///
- public Uri Uri => uri;
+ public Uri Uri { get; }
///
public override bool Equals(object? obj)
@@ -26,5 +35,5 @@ public override bool Equals(object? obj)
public override int GetHashCode() => Uri.GetHashCode();
///
- public override string? ToString() => uri.ToString();
+ public override string? ToString() => Uri.ToString();
}
diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs
index e8c84b69ec8..72d6e7b6b81 100644
--- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.cs
+++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/Configuration/ConfigurationServiceEndpointProvider.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 System.Diagnostics.CodeAnalysis;
-using System.Net;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ -41,7 +39,8 @@ 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);
+ var allowedSchemes = serviceDiscoveryOptions.Value.ApplyAllowedSchemes(query.IncludedSchemes);
+ _schemes = allowedSchemes as string[] ?? allowedSchemes.ToArray();
_configuration = configuration;
_logger = logger;
_options = options;
@@ -190,52 +189,18 @@ public ValueTask PopulateAsync(IServiceEndpointBuilder endpoints, CancellationTo
private void AddEndpoint(List endpoints, IConfigurationSection section, string endpointName)
{
- var value = section.Value;
- if (string.IsNullOrWhiteSpace(value) || !TryParseEndPoint(value, out var endPoint))
+ if (!ServiceEndpoint.TryParse(section.Value, out var serviceEndpoint))
{
throw new KeyNotFoundException($"The endpoint configuration section for service '{_serviceName}' endpoint '{endpointName}' has an invalid value with key '{section.Key}'.");
}
- endpoints.Add(CreateEndpoint(endPoint));
- }
-
- private static bool TryParseEndPoint(string value, [NotNullWhen(true)] out EndPoint? endPoint)
- {
- if (value.IndexOf("://") < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri))
- {
- var port = uri.Port > 0 ? uri.Port : 0;
- if (IPAddress.TryParse(uri.Host, out var ip))
- {
- endPoint = new IPEndPoint(ip, port);
- }
- else
- {
- 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.ShouldApplyHostNameMetadata(serviceEndpoint))
{
serviceEndpoint.Features.Set(this);
}
- return serviceEndpoint;
+ endpoints.Add(serviceEndpoint);
}
public override string ToString() => "Configuration";
diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs
index edc652507d9..a6fd7123aa7 100644
--- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.cs
+++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery/ServiceDiscoveryOptions.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.Collections.ObjectModel;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Extensions.ServiceDiscovery;
@@ -30,24 +31,30 @@ public sealed class ServiceDiscoveryOptions
///
public IList AllowedSchemes { get; set; } = new List();
- internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, IList allowedSchemes, bool allowAllSchemes)
+ ///
+ /// Filters the specified URI schemes to include only those that are applicable, based on the current settings.
+ ///
+ /// The URI schemes to be evaluated against the allowed schemes.
+ ///
+ /// The URI schemes that are applicable. If no schemes are requested, all allowed schemes are returned.
+ /// If all schemes are allowed, only the requested schemes are returned.
+ /// Otherwise, the intersection of requested and allowed schemes is returned.
+ ///
+ public IReadOnlyList ApplyAllowedSchemes(IReadOnlyList requestedSchemes)
{
- if (schemes.Count > 0)
+ ArgumentNullException.ThrowIfNull(requestedSchemes);
+
+ if (requestedSchemes.Count > 0)
{
- if (allowAllSchemes)
+ if (AllowAllSchemes)
{
- if (schemes is string[] array && array.Length > 0)
- {
- return array;
- }
-
- return schemes.ToArray();
+ return requestedSchemes;
}
List result = [];
- foreach (var s in schemes)
+ foreach (var s in requestedSchemes)
{
- foreach (var allowed in allowedSchemes)
+ foreach (var allowed in AllowedSchemes)
{
if (string.Equals(s, allowed, StringComparison.OrdinalIgnoreCase))
{
@@ -57,10 +64,10 @@ internal static string[] ApplyAllowedSchemes(IReadOnlyList schemes, ILis
}
}
- return result.ToArray();
+ return result.AsReadOnly();
}
// If no schemes were specified, but a set of allowed schemes were specified, allow those.
- return allowedSchemes.ToArray();
+ return new ReadOnlyCollection(AllowedSchemes);
}
}
From 1e5dc9490db87c63675825fa1fb7456e7cfe62fc Mon Sep 17 00:00:00 2001
From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com>
Date: Wed, 22 Oct 2025 15:39:44 +0200
Subject: [PATCH 2/2] Suppress false positive: string.IsNullOrWhiteSpace is not
annotated for nullability in netstandard/netfx
---
.../ServiceEndpoint.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs
index 541390c382e..e5a10374adc 100644
--- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs
+++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Abstractions/ServiceEndpoint.cs
@@ -65,7 +65,9 @@ public static bool TryParse([NotNullWhen(true)] string? value,
{
if (!string.IsNullOrWhiteSpace(value))
{
+#pragma warning disable CS8602
if (value.IndexOf("://", StringComparison.Ordinal) < 0 && Uri.TryCreate($"fakescheme://{value}", default, out var uri))
+#pragma warning restore CS8602
{
var port = uri.Port > 0 ? uri.Port : 0;
return IPAddress.TryParse(uri.Host, out var ip)