.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.
Table of Contents
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:
ResolveSrvandResolveSrvAsyncfor service location.ResolveMxandResolveMxAsyncfor mail exchangers.ResolveTxtandResolveTxtAsyncfor TXT data.ResolveCName,ResolvePtr, andResolveNsfor canonical names, reverse lookups, and authoritative servers.ResolveAddressesfor 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:
| Property | Meaning | Application use |
|---|---|---|
Records | The typed resource records in the answer | Build endpoints, mail routes, or policy values |
ResponseCode | The DNS protocol outcome | Distinguish success, NXDOMAIN, refusal, and server failure |
NegativeCacheTtl | How long a negative answer may be cached | Prevent 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, useNegativeCacheTtlrather 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
ResponseCodebefore 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
- Microsoft: What’s new in .NET 11 libraries — DNS record resolution APIs
- dotnet/runtime: System.Net.NameResolution public API reference
- dotnet/runtime: static typed DNS resolution implementation
- dotnet/runtime: configurable DnsResolver implementation and lifecycle
- RFC 2782: A DNS RR for specifying the location of services
Found this useful? Support more practical developer content.