.NET 11 DNS APIs can resolve typed SRV, MX, TXT, CNAME, PTR, NS, A, and AAAA records without a third-party DNS message parser. The result carries the records, the DNS response code, and negative-cache TTL metadata, so an application can make a real routing or configuration decision instead of treating every empty answer as the same failure.

The safe path is more than calling ResolveSrvAsync. Production code must preserve cancellation, interpret DnsResponseCode, respect record TTLs, apply SRV priority and weight correctly, and decide whether the system resolver or an explicitly configured DnsResolver owns the query. This guide builds that boundary without inventing a background DNS client.

What the .NET 11 DNS APIs add

The long-standing Dns.GetHostAddressesAsync and Dns.GetHostEntryAsync methods answer host-address questions. They do not expose the service location, mail routing, ownership text, authoritative server, canonical-name, or reverse-lookup records that infrastructure often publishes.

.NET 11 adds synchronous and asynchronous typed methods on System.Net.Dns:

  • ResolveSrv and ResolveSrvAsync for service location.
  • ResolveMx and ResolveMxAsync for mail exchangers.
  • ResolveTxt and ResolveTxtAsync for TXT data.
  • ResolveCName, ResolvePtr, and ResolveNs for canonical names, reverse lookups, and authoritative servers.
  • ResolveAddresses for typed A and AAAA answers with TTL information.

The async overloads accept a CancellationToken. Use them in request paths and hosted services so shutdown, request aborts, and bounded timeouts can stop work. These APIs are marked unsupported on Android and WASI; treat platform support as a deployment constraint, not a runtime surprise.

Treat DnsResult as a protocol result

Every typed query returns DnsResult<TRecord>. Its three properties carry separate information:

PropertyMeaningApplication use
RecordsThe typed resource records in the answerBuild endpoints, mail routes, or policy values
ResponseCodeThe DNS protocol outcomeDistinguish success, NXDOMAIN, refusal, and server failure
NegativeCacheTtlHow long a negative answer may be cachedPrevent repeated failed lookups without caching absence forever

Do not reduce that contract to Records.Count == 0. An empty successful response and NxDomain are different operational events. ServerFailure and Refused usually deserve a short retry policy or a surfaced dependency failure, not a long negative-cache entry.

using System.Net;

static void EnsureUsableResponse<T>(DnsResult<T> result, string queryName)
{
    if (result.ResponseCode == DnsResponseCode.NoError)
    {
        return;
    }

    if (result.ResponseCode == DnsResponseCode.NxDomain)
    {
        throw new InvalidOperationException(
            $"DNS name '{queryName}' does not exist. " +
            $"Negative TTL: {result.NegativeCacheTtl}.");
    }

    throw new InvalidOperationException(
        $"DNS query for '{queryName}' failed with {result.ResponseCode}.");
}

A library may prefer a result union instead of exceptions. The essential requirement is that the caller can still tell “no such name” from “the resolver failed” and decide whether fallback is safe.

Resolve and select SRV records

SRV records publish a target host and port for a named service. The query name normally follows _service._protocol.host, such as _https._tcp.api.example.com. Each SrvRecord exposes Target, Port, Priority, Weight, Ttl, and resolved target Addresses.

using System.Net;

public static async Task<IReadOnlyList<SrvRecord>> ResolveServiceAsync(
    string serviceName,
    CancellationToken cancellationToken)
{
    DnsResult<SrvRecord> result =
        await Dns.ResolveSrvAsync(serviceName, cancellationToken);

    EnsureUsableResponse(result, serviceName);

    if (result.Records.Count == 0)
    {
        throw new InvalidOperationException(
            $"DNS returned no SRV records for '{serviceName}'.");
    }

    if (result.Records.Any(record => record.Target == "."))
    {
        throw new InvalidOperationException(
            $"SRV reports that '{serviceName}' is unavailable.");
    }

    return result.Records;
}

An SRV target of . is not an empty hostname. RFC 2782 uses it to state that the service is unavailable at this domain. Fail closed instead of attempting to connect or silently falling back to A or AAAA records unless the application has an explicit non-SRV fallback policy.

Priority is ordered; weight is probabilistic

Always choose from the lowest available Priority value first. Within that priority group, Weight is not another descending sort key. It expresses a relative probability. Sorting by weight would send every request to one endpoint and defeat the load-distribution intent.

static SrvRecord SelectSrvTarget(
    IReadOnlyList<SrvRecord> records,
    Random random)
{
    ushort bestPriority = records.Min(record => record.Priority);
    SrvRecord[] eligible = records
        .Where(record => record.Priority == bestPriority)
        .ToArray();

    // Randomize ties, then place zero-weight records first as RFC 2782 requires.
    for (int i = eligible.Length - 1; i > 0; i--)
    {
        int j = random.Next(i + 1);
        (eligible[i], eligible[j]) = (eligible[j], eligible[i]);
    }

    eligible = eligible
        .OrderBy(record => record.Weight == 0 ? 0 : 1)
        .ToArray();

    int totalWeight = eligible.Sum(record => record.Weight);
    if (totalWeight == 0)
    {
        return eligible[random.Next(eligible.Length)];
    }

    int ticket = random.Next(totalWeight + 1);
    int runningWeight = 0;

    foreach (SrvRecord record in eligible)
    {
        runningWeight += record.Weight;
        if (runningWeight >= ticket)
        {
            return record;
        }
    }

    return eligible[^1];
}

This follows RFC 2782’s inclusive draw from zero through the sum of the weights. Zero-weight records are placed first, and the initial shuffle prevents one fixed zero-weight record from always owning the small chance represented by ticket zero. When every weight is zero, selection is uniform. To build a complete fallback order, repeat the selection after removing each chosen record.

Selection is only the first attempt. A robust client should retain the other eligible records for connection fallback, record which target was attempted, and bound the total connection budget. DNS success does not prove that the advertised service is accepting traffic.

Handle MX and TXT records

MxRecord exposes Exchange, Preference, and Ttl. Lower preference values are attempted first. Unlike SRV, MX does not have a weight field.

DnsResult<MxRecord> mx =
    await Dns.ResolveMxAsync("example.com", cancellationToken);

EnsureUsableResponse(mx, "example.com");

IReadOnlyList<MxRecord> mailRoutes = mx.Records
    .OrderBy(record => record.Preference)
    .ToArray();

A TxtRecord exposes Values and Ttl. One logical TXT record can contain multiple character strings. Join or interpret those values according to the protocol using the record; do not assume every string is an independent record, and do not treat untrusted TXT content as executable configuration.

DnsResult<TxtRecord> txt =
    await Dns.ResolveTxtAsync("example.com", cancellationToken);

EnsureUsableResponse(txt, "example.com");

foreach (TxtRecord record in txt.Records)
{
    string logicalValue = string.Concat(record.Values);
    Console.WriteLine($"TTL={record.Ttl} Value={logicalValue}");
}

TXT is often used for domain verification and mail policy, but the typed API does not validate SPF, DKIM, DMARC, or an application-specific ownership token. Parsing and policy validation remain the caller’s responsibility.

Use a custom DNS resolver deliberately

The static Dns.Resolve* methods use system-configured DNS servers. That is the correct default for most applications because it follows the host, container, VPN, and cluster networking policy.

Use DnsResolver when the application must query an explicit endpoint—for example, a private service-discovery server that is part of the deployment contract. Register one long-lived resolver; the implementation is thread-safe and supports concurrent queries.

using System.Net;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton(_ =>
{
    var options = new DnsResolverOptions
    {
        Servers = new List<IPEndPoint>
        {
            new(IPAddress.Parse("10.0.0.53"), 53)
        }
    };

    return new DnsResolver(options);
});

The container owns the singleton and disposes it during shutdown. Do not hard-code a public resolver to bypass enterprise split-horizon DNS or platform policy. Put endpoints in validated configuration, use only trusted servers, and verify that the configured server is reachable from every deployment environment.

Define the failure policy at the call site

A DNS timeout should not automatically fall back to a stale or public answer. The correct fallback depends on the record’s job. Service discovery may use a recently expired last-known-good endpoint for a very short grace window; ownership verification should normally fail closed; telemetry enrichment may skip the value and continue. Make that policy explicit and observable.

Cache answers without hiding changes

The first version of the typed resolver surface does not provide an application-level cache contract. If a hot path performs the same query repeatedly, cache the interpreted answer in your application and derive expiration from DNS metadata.

  • For positive answers, do not keep the aggregate longer than the smallest relevant record TTL.
  • For NxDomain, use NegativeCacheTtl rather than a permanent “not found” marker.
  • For resolver failures, prefer a short retry/backoff policy; a server error is not proof that the name does not exist.
  • Apply a local maximum TTL when operational policy requires faster convergence than the published value.
  • Coalesce concurrent refreshes so an expired popular name does not trigger a lookup stampede.
static TimeSpan BoundPositiveTtl(IReadOnlyList<SrvRecord> records)
{
    TimeSpan published = records.Min(record => record.Ttl);
    TimeSpan minimum = TimeSpan.FromSeconds(5);
    TimeSpan maximum = TimeSpan.FromMinutes(5);

    return published < minimum
        ? minimum
        : published > maximum
            ? maximum
            : published;
}

The five-second floor above is an application choice, not a DNS rule. Very short published TTLs may signal deliberate rapid failover. If the local floor changes that behavior, document the tradeoff and monitor stale-target connection failures.

Verify behavior before deployment

A successful compile does not prove the deployment can reach its resolver or see the same records as a developer workstation. Add a bounded startup diagnostic, health probe, or operator command that logs the query name, response code, record count, TTL range, resolver class, and elapsed time. Do not log sensitive TXT values.

using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(3));

DnsResult<SrvRecord> result = await Dns.ResolveSrvAsync(
    "_https._tcp.api.example.com",
    timeout.Token);

Console.WriteLine(
    $"Code={result.ResponseCode} " +
    $"Records={result.Records.Count} " +
    $"NegativeTtl={result.NegativeCacheTtl}");

foreach (SrvRecord record in result.Records)
{
    Console.WriteLine(
        $"Target={record.Target} Port={record.Port} " +
        $"Priority={record.Priority} Weight={record.Weight} TTL={record.Ttl}");
}

Run the same probe in the target network namespace or container, not only on a laptop. Verify one successful record set, an intentionally missing name, cancellation, and the response to an unavailable configured resolver. Those four paths prove more than a screenshot of a single successful lookup.

For automated tests, keep selection, TTL bounding, response-code mapping, and fallback policy in pure functions and test them with constructed inputs. Reserve live DNS checks for an integration environment you control; public DNS records can change and should not make unit tests flaky.

Production checklist

  • Use async methods with cancellation and an explicit end-to-end time budget.
  • Handle ResponseCode before consuming records.
  • For SRV, select the lowest priority group and apply weight probabilistically.
  • Treat an SRV target of . as an explicit service-unavailable result.
  • Retain alternative targets for bounded connection fallback.
  • Cache positive and negative outcomes according to their different TTL semantics.
  • Use the system resolver unless a custom server is an explicit deployment requirement.
  • Check Android and WASI restrictions before sharing the resolver component across targets.
  • Log metadata and timing, but do not expose sensitive TXT content.
  • Revalidate preview API names and platform attributes before publishing the article.

The value of the .NET 11 DNS APIs is not just a longer list of query methods. Typed records and protocol metadata let the application preserve the decisions DNS was designed to communicate. When cancellation, response codes, selection rules, TTLs, and resolver ownership are treated as one boundary, service discovery becomes testable instead of accidental.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Write A Comment

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock