diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.cs new file mode 100644 index 00000000000..0cc256bd3c3 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptions.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. + +using System.Net; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +/// +/// Provides configuration options for DNS resolution, including server endpoints, retry attempts, and timeout settings. +/// +public class DnsResolverOptions +{ + /// + /// Gets or sets the collection of server endpoints used for network connections. + /// + public IList Servers { get; set; } = new List(); + + /// + /// Gets or sets the maximum number of attempts per server. + /// + public int MaxAttempts { get; set; } = 2; + + /// + /// Gets or sets the maximum duration per attempt to wait before timing out. + /// + /// + /// The maximum time for resolving a query is * count * . + /// + public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(3); + + // override for testing purposes + internal Func, int, int>? _transportOverride; +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs new file mode 100644 index 00000000000..d61da5e5e0b --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsResolverOptionsValidator.cs @@ -0,0 +1,41 @@ +// 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 Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns; + +internal sealed class DnsResolverOptionsValidator : IValidateOptions +{ + // CancellationTokenSource.CancelAfter has a maximum timeout of Int32.MaxValue milliseconds. + private static readonly TimeSpan s_maxTimeout = TimeSpan.FromMilliseconds(int.MaxValue); + + public ValidateOptionsResult Validate(string? name, DnsResolverOptions options) + { + if (options.Servers is null) + { + return ValidateOptionsResult.Fail($"{nameof(options.Servers)} must not be null."); + } + + if (options.MaxAttempts < 1) + { + return ValidateOptionsResult.Fail($"{nameof(options.MaxAttempts)} must be one or greater."); + } + + if (options.Timeout != Timeout.InfiniteTimeSpan) + { + if (options.Timeout <= TimeSpan.Zero) + { + return ValidateOptionsResult.Fail($"{nameof(options.Timeout)} must not be negative or zero."); + } + + if (options.Timeout > s_maxTimeout) + { + return ValidateOptionsResult.Fail($"{nameof(options.Timeout)} must not be greater than {s_maxTimeout.TotalMilliseconds} milliseconds."); + } + } + + return ValidateOptionsResult.Success; + } +} diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs index 57820560a63..ef593a7340c 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderFactory.cs @@ -22,6 +22,8 @@ internal sealed partial class DnsSrvServiceEndpointProviderFactory( /// public bool TryCreateProvider(ServiceEndpointQuery query, [NotNullWhen(true)] out IServiceEndpointProvider? provider) { + var optionsValue = options.CurrentValue; + // 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. @@ -30,19 +32,26 @@ 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". - if (string.IsNullOrWhiteSpace(_querySuffix)) + if (optionsValue.ServiceDomainNameCallback == null && string.IsNullOrWhiteSpace(_querySuffix)) { DnsServiceEndpointProviderBase.Log.NoDnsSuffixFound(logger, query.ToString()!); provider = default; return false; } - var portName = query.EndpointName ?? "default"; - var srvQuery = $"_{portName}._tcp.{query.ServiceName}.{_querySuffix}"; + var srvQuery = optionsValue.ServiceDomainNameCallback != null + ? optionsValue.ServiceDomainNameCallback(query) + : DefaultServiceDomainNameCallback(query, optionsValue); provider = new DnsSrvServiceEndpointProvider(query, srvQuery, hostName: query.ServiceName, options, logger, resolver, timeProvider); return true; } + private static string DefaultServiceDomainNameCallback(ServiceEndpointQuery query, DnsSrvServiceEndpointProviderOptions options) + { + var portName = query.EndpointName ?? "default"; + return $"_{portName}._tcp.{query.ServiceName}.{options.QuerySuffix}"; + } + private static string? GetKubernetesHostDomain() { // Check that we are running in Kubernetes first. diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs index c908c56d770..c1d64136cc9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/DnsSrvServiceEndpointProviderOptions.cs @@ -36,6 +36,11 @@ public class DnsSrvServiceEndpointProviderOptions /// public string? QuerySuffix { get; set; } + /// + /// Gets or sets a delegate that generates a DNS SRV query from a specified instance. + /// + public Func? ServiceDomainNameCallback { get; set; } + /// /// Gets or sets a delegate used to determine whether to apply host name metadata to each resolved endpoint. Defaults to false. /// diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs index bc290c6b907..511e8fdb1c9 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/DnsResolver.cs @@ -11,6 +11,7 @@ using System.Security.Cryptography; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; @@ -19,43 +20,40 @@ 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 DnsResolverOptions _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) + public DnsResolver(TimeProvider timeProvider, ILogger logger, IOptions options) { _timeProvider = timeProvider; _logger = logger; - _options = options; - Debug.Assert(_options.Servers.Count > 0); + _options = options.Value; - if (options.Timeout != Timeout.InfiniteTimeSpan) + if (_options.Servers.Count == 0) { - ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(options.Timeout, TimeSpan.Zero); - ArgumentOutOfRangeException.ThrowIfGreaterThan(options.Timeout, s_maxTimeout); - } - } - - internal DnsResolver(ResolverOptions options) : this(TimeProvider.System, NullLogger.Instance, options) - { - } + foreach (var server in OperatingSystem.IsLinux() || OperatingSystem.IsMacOS() + ? ResolvConf.GetServers() + : NetworkInfo.GetServers()) + { + _options.Servers.Add(server); + } - internal DnsResolver(IEnumerable servers) : this(new ResolverOptions(servers.ToArray())) - { + if (_options.Servers.Count == 0) + { + throw new ArgumentException("At least one DNS server is required.", nameof(options)); + } + } } - internal DnsResolver(IPEndPoint server) : this(new ResolverOptions(server)) + // This constructor is for unit testing only. Does not auto-add system DNS servers. + internal DnsResolver(DnsResolverOptions options) { + _timeProvider = TimeProvider.System; + _logger = NullLogger.Instance; + _options = options; } public ValueTask ResolveServiceAsync(string name, CancellationToken cancellationToken = default) @@ -365,7 +363,7 @@ internal struct SendQueryResult { IPEndPoint serverEndPoint = _options.Servers[index]; - for (int attempt = 1; attempt <= _options.Attempts; attempt++) + for (int attempt = 1; attempt <= _options.MaxAttempts; attempt++) { DnsResponse response = default; try diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs index c2ef13f922e..24b5155a1c8 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/NetworkInfo.cs @@ -8,8 +8,8 @@ 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() + // basic option to get DNS servers via NetworkInfo. We may get it directly later via proper APIs. + public static IList GetServers() { List servers = new List(); @@ -31,6 +31,6 @@ public static ResolverOptions GetOptions() } } - return new ResolverOptions(servers); + return servers; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs index fbfdc5ae027..de68e88c18d 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolvConf.cs @@ -10,12 +10,12 @@ internal static class ResolvConf { [SupportedOSPlatform("linux")] [SupportedOSPlatform("osx")] - public static ResolverOptions GetOptions() + public static IList GetServers() { - return GetOptions(new StreamReader("/etc/resolv.conf")); + return GetServers(new StreamReader("/etc/resolv.conf")); } - public static ResolverOptions GetOptions(TextReader reader) + public static IList GetServers(TextReader reader) { List serverList = new(); @@ -40,9 +40,9 @@ public static ResolverOptions GetOptions(TextReader reader) 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 NetworkInfo.GetServers(); } - return new ResolverOptions(serverList); + return serverList; } } diff --git a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs deleted file mode 100644 index 51d03f64bfd..00000000000 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/Resolver/ResolverOptions.cs +++ /dev/null @@ -1,31 +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.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/ServiceDiscoveryDnsServiceCollectionExtensions.cs b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs index 42f220445b1..313e68b3b8a 100644 --- a/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns/ServiceDiscoveryDnsServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery; using Microsoft.Extensions.ServiceDiscovery.Dns; using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; @@ -59,24 +60,10 @@ public static IServiceCollection AddDnsSrvServiceEndpointProvider(this IServiceC services.AddSingleton(); var options = services.AddOptions(); - options.Configure(o => configureOptions?.Invoke(o)); + options.Configure(configureOptions); + 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; - } } /// @@ -109,9 +96,55 @@ public static IServiceCollection AddDnsServiceEndpointProvider(this IServiceColl ArgumentNullException.ThrowIfNull(configureOptions); services.AddServiceDiscoveryCore(); + + if (!GetDnsClientFallbackFlag()) + { + services.TryAddSingleton(); + } + else + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } + services.AddSingleton(); var options = services.AddOptions(); - options.Configure(o => configureOptions?.Invoke(o)); + options.Configure(configureOptions); + + return services; + } + + /// + /// Configures the DNS resolver used for service discovery. + /// + /// The service collection. + /// The DNS resolver options. + /// The provided . + public static IServiceCollection ConfigureDnsResolver(this IServiceCollection services, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); + + var options = services.AddOptions(); + options.Configure(configureOptions); + services.AddTransient, DnsResolverOptionsValidator>(); return services; } + + private 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; + } + } 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 index 1b180d74b9d..2e4658e3ba2 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.Fuzzing/Fuzzers/DnsResponseFuzzer.cs @@ -19,10 +19,11 @@ public void FuzzTarget(ReadOnlySpan data) if (_resolver == null) { _buffer = new byte[4096]; - _resolver = new DnsResolver(new ResolverOptions(new IPEndPoint(IPAddress.Loopback, 53)) + _resolver = new DnsResolver(new DnsResolverOptions { + Servers = [new IPEndPoint(IPAddress.Loopback, 53)], Timeout = TimeSpan.FromSeconds(5), - Attempts = 1, + MaxAttempts = 1, _transportOverride = (buffer, length) => { // the first two bytes are the random transaction ID, so we keep that @@ -41,4 +42,4 @@ public void FuzzTarget(ReadOnlySpan data) 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/DnsServicePublicApiTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs index e347deb9822..69bb6e0e510 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsServicePublicApiTests.cs @@ -68,12 +68,23 @@ public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenServ } [Fact] - public void AddDnsServiceEndpointProviderWithConfigureOptionsShouldThrowWhenConfigureOptionsIsNull() + public void ConfigureDnsResolverShouldThrowWhenServicesIsNull() + { + IServiceCollection services = null!; + + var action = () => services.ConfigureDnsResolver(_ => { }); + + var exception = Assert.Throws(action); + Assert.Equal(nameof(services), exception.ParamName); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenConfigureOptionsIsNull() { IServiceCollection services = new ServiceCollection(); - Action configureOptions = null!; + Action configureOptions = null!; - var action = () => services.AddDnsServiceEndpointProvider(configureOptions); + var action = () => services.ConfigureDnsResolver(configureOptions); var exception = Assert.Throws(action); Assert.Equal(nameof(configureOptions), exception.ParamName); 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 14abd659029..6d2aba6cb64 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/LoopbackDnsTestBase.cs @@ -5,6 +5,8 @@ using System.Text; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; using Microsoft.Extensions.ServiceDiscovery.Dns.Tests; using Microsoft.Extensions.Time.Testing; using Xunit.Abstractions; @@ -18,7 +20,7 @@ public abstract class LoopbackDnsTestBase : IDisposable internal LoopbackDnsServer DnsServer { get; } private readonly Lazy _resolverLazy; internal DnsResolver Resolver => _resolverLazy.Value; - internal ResolverOptions Options { get; } + internal DnsResolverOptions Options { get; } protected readonly FakeTimeProvider TimeProvider; public LoopbackDnsTestBase(ITestOutputHelper output) @@ -26,10 +28,11 @@ public LoopbackDnsTestBase(ITestOutputHelper output) Output = output; DnsServer = new(); TimeProvider = new(); - Options = new([DnsServer.DnsEndPoint]) + Options = new() { + Servers = [DnsServer.DnsEndPoint], Timeout = TimeSpan.FromSeconds(5), - Attempts = 1, + MaxAttempts = 1, }; _resolverLazy = new(InitializeResolver); } @@ -39,8 +42,7 @@ 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); + var resolver = new DnsResolver(TimeProvider, NullLogger.Instance, new OptionsWrapper(Options)); return resolver; } diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs index 281ffbecd24..4c2bcadd8a5 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/ResolvConfTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.Extensions.ServiceDiscovery.Dns.Resolver.Tests; public class ResolvConfTests { [Fact] - public void GetOptions() + public void GetServers() { var contents = @" nameserver 10.96.0.10 @@ -18,9 +18,9 @@ search default.svc.cluster.local svc.cluster.local cluster.local @"; var reader = new StringReader(contents); - ResolverOptions options = ResolvConf.GetOptions(reader); + var servers = ResolvConf.GetServers(reader); - IPEndPoint ipAddress = Assert.Single(options.Servers); + IPEndPoint ipAddress = Assert.Single(servers); Assert.Equal(new IPEndPoint(IPAddress.Parse("10.96.0.10"), 53), ipAddress); } } 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 49985846570..3d6f3724484 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/RetryTests.cs @@ -11,7 +11,7 @@ public class RetryTests : LoopbackDnsTestBase { public RetryTests(ITestOutputHelper output) : base(output) { - Options.Attempts = 3; + Options.MaxAttempts = 3; } private Task SetupUdpProcessFunction(LoopbackDnsServer server, Func func) @@ -49,7 +49,7 @@ public async Task Retry_Simple_Success() Task t = SetupUdpProcessFunction(builder => { attempt++; - if (attempt == Options.Attempts) + if (attempt == Options.MaxAttempts) { builder.Answers.AddAddress(hostName, 3600, address); } @@ -214,7 +214,7 @@ public async Task ExhaustedRetries_FailoverToNextServer() return Task.CompletedTask; }); - Assert.Equal(Options.Attempts, primaryAttempt); + Assert.Equal(Options.MaxAttempts, primaryAttempt); Assert.Equal(1, secondaryAttempt); AddressResult res = Assert.Single(results); 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 cbdb5e282e9..b2891cfb512 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Resolver/TcpFailoverTests.cs @@ -42,7 +42,7 @@ public async Task TcpFailover_Simple_Success() public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() { string hostName = "tcp-server-closes.test"; - Options.Attempts = 1; + Options.MaxAttempts = 1; Options.Timeout = TimeSpan.FromSeconds(60); _ = DnsServer.ProcessUdpRequest(builder => @@ -66,7 +66,7 @@ public async Task TcpFailover_ServerClosesWithoutData_EmptyResult() public async Task TcpFailover_TcpNotAvailable_EmptyResult() { string hostName = "tcp-not-available.test"; - Options.Attempts = 1; + Options.MaxAttempts = 1; Options.Timeout = TimeSpan.FromMilliseconds(100000); _ = DnsServer.ProcessUdpRequest(builder => diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000000..3631c2e8085 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/ServiceDiscoveryDnsServiceCollectionExtensionsTests.cs @@ -0,0 +1,92 @@ +// 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.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests; + +public class ServiceDiscoveryDnsServiceCollectionExtensionsTests +{ + [Fact] + public void AddDnsServiceEndpointProviderShouldRegisterDependentServices() + { + var services = new ServiceCollection(); + services.AddDnsServiceEndpointProvider(); + + using var serviceProvider = services.BuildServiceProvider(true); + + var exception = Record.Exception(() => serviceProvider.GetServices()); + Assert.Null(exception); + } + + [Fact] + public void AddDnsSrvServiceEndpointProviderShouldRegisterDependentServices() + { + var services = new ServiceCollection(); + services.AddDnsSrvServiceEndpointProvider(); + + using var serviceProvider = services.BuildServiceProvider(true); + + var exception = Record.Exception(() => serviceProvider.GetServices()); + Assert.Null(exception); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenServersIsNull() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Servers = null!); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Servers must not be null.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenMaxAttemptsIsZero() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.MaxAttempts = 0); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("MaxAttempts must be one or greater.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenTimeoutIsZero() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Timeout = TimeSpan.Zero); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Timeout must not be negative or zero.", exception.Message); + } + + [Fact] + public void ConfigureDnsResolverShouldThrowWhenTimeoutExceedsMaximum() + { + var services = new ServiceCollection(); + services.ConfigureDnsResolver(options => options.Timeout = TimeSpan.FromMilliseconds(1L + int.MaxValue)); + + using var serviceProvider = services.BuildServiceProvider(); + var options = serviceProvider.GetRequiredService>(); + + var exception = Assert.Throws(() => options.Value); + Assert.Equal("Timeout must not be greater than 2147483647 milliseconds.", exception.Message); + } +} diff --git a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs index c2751823c65..d38814621c4 100644 --- a/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs +++ b/test/Libraries/Microsoft.Extensions.ServiceDiscovery.Yarp.Tests/YarpServiceDiscoveryTests.cs @@ -1,16 +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.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Xunit; -using Yarp.ReverseProxy.Configuration; using System.Net; using System.Net.Sockets; -using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.ServiceDiscovery.Dns; +using Microsoft.Extensions.ServiceDiscovery.Dns.Resolver; +using Yarp.ReverseProxy.Configuration; namespace Microsoft.Extensions.ServiceDiscovery.Yarp.Tests; @@ -232,7 +232,7 @@ public async Task ServiceDiscoveryDestinationResolverTests_Configuration_Disallo [Fact] public async Task ServiceDiscoveryDestinationResolverTests_Dns() { - DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance); + DnsResolver resolver = new DnsResolver(TimeProvider.System, NullLogger.Instance, new OptionsWrapper(new DnsResolverOptions())); await using var services = new ServiceCollection() .AddSingleton(resolver)