4 min readThe rules of the call

The analyzers that catch real bugs

NResilience ships with built-in Roslyn analyzers that catch common resilience bugs at compile time, from token mistakes to unreachable timeouts.

analyzerstooling

TL;DR: Resilience is a domain where the most dangerous bugs are silent. A wrong cancellation token or a contradictory timeout doesn't cause a crash; it just disables your protection. NResilience ships with built-in analyzers (NRES001-004) that turn these silent failures into compile-time warnings.

Why analyzers?

If you write a bug in your business logic, it usually throws an exception or produces a wrong value. You find it in a test or a log.

Resilience bugs are different. If you pass the wrong cancellation token into an HTTP call, the code still compiles. The call still runs. But your AttemptTimeout is now a lie - it's being tracked by the library, but ignored by the socket. You only discover the bug when your production system hangs under load because a dependency stopped responding and your "timeouts" didn't fire.

The only way to stop this is to make the compiler understand the intent of the resilience loop.

The Reliability Guards (NRES001 & NRES002)

The most common integration bug is the "Token Swap." There is a caller's token and an attempt token - two different tokens for two different parts of the call.

NRES001: Token Not Passed If you call a method that takes a CancellationToken inside your callback but don't provide one, the analyzer warns you.

  • The bug: The work is not linked to the attempt. If the attempt times out, the work keeps running.

NRES002: Wrong Token Passed If you pass a token other than the attempt token (like the caller's token or CancellationToken.None), the analyzer flags it.

  • The bug: You've silenced the AttemptTimeout. The executor will stop waiting, but the request will stay alive until the socket times out.
// ❌ NRES002: the attempt timeout has no effect on this call
await policy.RunAsync(
    attempt => http.GetAsync(url, callerToken),
    callerToken);

// ✅ The fix: pass the attempt token
await policy.RunAsync(
    attempt => http.GetAsync(url, attempt),
    callerToken);

The Configuration Guards (NRES003 & NRES004)

Resilience policies are values. with expressions make it easy to state any configuration - including logically impossible ones.

NRES003: Invalid Configuration NResilience validates policies at runtime, on first execution. But when a policy is built from literals - a static readonly field, say - the analyzer can fold those constants and run the same validation at compile time. If you set Attempts = 0 or a negative Deadline, you get a warning before you even hit "Run."

NRES004: Unreachable Bounds This catches the "dead configuration" problem. If your AttemptTimeout is 60 seconds, but your Deadline is 30 seconds, the 60-second bound can never be reached.

  • The bug: The developer thinks they have a 60-second buffer per attempt, but in reality, the call will be killed at 30 seconds. The analyzer flags this so you can align your bounds.
// ❌ NRES004: AttemptTimeout > Deadline
var policy = Resilience.Default with 
{ 
    Deadline = TimeSpan.FromSeconds(30),
    AttemptTimeout = TimeSpan.FromSeconds(60) 
};

The warning does the arithmetic rather than only naming the conflict: AttemptTimeout (60s) is longer than Deadline (30s); attempt 1 is clamped to 30s, 50% of the attempt timeout, so this setting can never be reached.

That clamp clause is the same sentence policy.Explain() prints at runtime, in the same duration format. An analyzer cannot reference the library it analyses - it targets netstandard2.0 and loads into the compiler's own process - so they share the format instead of the code, ensuring the IDE and startup log use the same sentence.

Shipped in the box

Unlike many libraries that put analyzers in a separate .Analyzers NuGet package, NResilience ships them inside the main package.

When you add the NResilience reference to your project, the analyzers load automatically with the compiler. There is no extra setup, no separate package to track, and no way to "forget" to install the protection. If you are using the library, you are using the analyzers.

Summary

By moving resilience knowledge into the compiler, NResilience eliminates the "hope it works" phase of integration. Instead of relying on a developer remembering a nuance about cancellation tokens, the IDE tells you exactly what is wrong and, in many cases, provides the code fix to solve it.

The analyzers catch more than compile-time bugs. The NRES005-008 design diagnostics act as a code reviewer: they flag reliability guard objects (breakers, budgets, scopes) built per call, resilient HttpClients created per call, needlessly async callbacks, and latency-estimate policies rebuilt per call - mistakes that are legal but pointless. The analyzer reference documents the full set.

Once the analyzers have caught the integration bugs, the next layer of protection is the circuit breaker. Open the breaker before the dependency drowns covers how the breaker detects brownouts, manages recovery ramps, and stops calls to a struggling dependency before it drowns.