RESTCLIENT.NET / DOCUMENTATION
Exhaustion Analyzer
The Exhaustion analyzer is a Roslyn analyzer that ensures your switch expressions handle all possible cases of discriminated unions at compile time.
The Problem
C#'s default switch exhaustiveness checking is limited. This code compiles but crashes at runtime:
// This compiles without warnings!
var output = result switch
{
OkPost(var post) => $"Success: {post.Title}",
ErrorPost(ResponseErrorPost(var err, var status, _)) => $"Error {status}",
// Missing ExceptionErrorPost case - runtime crash!
};
When an ExceptionError occurs, you get a MatchException at runtime. In production. On a Friday afternoon.
The Solution
With Exhaustion installed, the same code produces a compile error:
error EXHAUSTION001: Switch on Result is not exhaustive;
Matched: Ok<Post, HttpError<ErrorResponse>>, Error<Post, HttpError<ErrorResponse>> with ErrorResponseError<ErrorResponse>
Missing: Error<Post, HttpError<ErrorResponse>> with ExceptionError<ErrorResponse>
Runtime crashes become compile-time errors.
Installation
With RestClient.Net
Exhaustion is automatically included:
dotnet add package RestClient.Net
Standalone
For use with your own discriminated unions:
dotnet add package Exhaustion
How It Works
Exhaustion uses Roslyn's analyzer API to:
- Detect switch expressions on Result types
- Analyze which patterns are matched
- Determine which cases are missing
- Report compile-time errors for incomplete switches
Supported Types
Exhaustion works with:
Result<TSuccess, TError>from RestClient.NetHttpError<TError>from RestClient.Net- Custom discriminated unions following the same pattern
Error Messages
EXHAUSTION001: Non-exhaustive switch
error EXHAUSTION001: Switch on Result is not exhaustive;
Matched: Ok<User, HttpError<ApiError>>
Missing: Error<User, HttpError<ApiError>> with ErrorResponseError, Error<User, HttpError<ApiError>> with ExceptionError
The error tells you:
- What type the switch is on
- Which cases ARE matched
- Which cases are MISSING
Examples
Missing HttpError Cases
// ERROR: Missing both error cases
var message = result switch
{
OkUser(var user) => user.Name,
};
// ERROR: Missing ExceptionError
var message = result switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
};
// CORRECT: All cases handled
var message = result switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
ErrorUser(ExceptionErrorUser(var ex)) => ex.Message,
};
Using Discard Pattern
You can use discard for cases you want to handle uniformly:
// CORRECT: Discard covers remaining cases
var message = result switch
{
OkUser(var user) => user.Name,
_ => "Error occurred",
};
However, this defeats the purpose of exhaustiveness checking. Use it sparingly.
Multiple Result Types
Each Result type needs its own handling:
var (userResult, ordersResult) = await (GetUserAsync(), GetOrdersAsync());
// Both must be exhaustively handled
var userMessage = userResult switch
{
OkUser(var user) => user.Name,
ErrorUser(ResponseErrorUser(var err, _, _)) => err.Message,
ErrorUser(ExceptionErrorUser(var ex)) => ex.Message,
};
var ordersMessage = ordersResult switch
{
OkOrders(var orders) => $"{orders.Count} orders",
ErrorOrders(ResponseErrorOrders(var err, _, _)) => err.Message,
ErrorOrders(ExceptionErrorOrders(var ex)) => ex.Message,
};
Creating Exhaustive Custom Types
You can use Exhaustion with your own discriminated unions:
// Define a discriminated union
public abstract record PaymentResult
{
public sealed record Success(string TransactionId) : PaymentResult;
public sealed record Declined(string Reason) : PaymentResult;
public sealed record Error(Exception Exception) : PaymentResult;
}
// Exhaustion will check switches on PaymentResult
var message = paymentResult switch
{
PaymentResult.Success(var txId) => $"Paid: {txId}",
PaymentResult.Declined(var reason) => $"Declined: {reason}",
PaymentResult.Error(var ex) => $"Error: {ex.Message}",
};
Configuration
Suppress for Specific Lines
In rare cases, suppress the warning:
#pragma warning disable EXHAUSTION001
var message = result switch
{
OkUser(var user) => user.Name,
// Intentionally incomplete
};
#pragma warning restore EXHAUSTION001
Global Suppression
In .editorconfig (not recommended):
[*.cs]
dotnet_diagnostic.EXHAUSTION001.severity = none
IDE Integration
Visual Studio
Exhaustion errors appear in the Error List with full details. Click the error to navigate to the problematic switch.
VS Code
With the C# extension, errors appear in the Problems panel and as red squiggles in the editor.
Rider
JetBrains Rider fully supports Roslyn analyzers. Errors appear in the inspection results.
Benefits
- Catch errors early - Compile time, not runtime
- Safer refactoring - Add a new case? Compiler finds all switches
- Self-documenting - The types show all possible outcomes
- Better code review - Reviewers don't need to check for missing cases
Best Practices
- Never suppress without a comment explaining why
- Avoid discard patterns (
_) unless intentional - Handle each case explicitly for clarity
- Use type aliases for cleaner pattern matching
- Let the analyzer guide you when types change
Troubleshooting
Analyzer Not Working
Ensure the package is installed:
dotnet list package | grep Exhaustion
Rebuild the project:
dotnet build --no-incremental
False Positives
If Exhaustion reports an error incorrectly, please report an issue.
Next Steps
- Error Handling - Result type patterns
- Basic Usage - Getting started guide
- API Reference - Result type documentation
Bounded analysis
Exhaustion limits how much work it performs when exploring large or recursive type hierarchies. When that budget is exceeded, EXHAUSTION002 reports that analysis could not complete. Simplify the switch or the modeled hierarchy before treating it as exhaustively checked. The analyzer does not claim completeness when it reaches this limit.