The Updown.io .NET client is a lightweight, asynchronous HTTP API client library that provides a strongly-typed interface for interacting with the Updown.io monitoring service. The library is designed with modern .NET practices, including nullable reference types, comprehensive error handling, and full async/await support.
The library aims to be as simple to use as possible while remaining powerful. Most common operations can be completed with just a few lines of code.
- Full support for async/await patterns
- Nullable reference types enabled
- CancellationToken support throughout
- Proper exception handling with custom exception types
- XML documentation for IntelliSense support
The library maintains backward compatibility through:
- Obsolete attributes on deprecated methods
- Maintaining old property names while introducing better-named alternatives
- Careful versioning strategies
Supports multiple target frameworks:
- .NET 9.0
- .NET 8.0
- .NET 6.0
- .NET Standard 2.0
The main entry point for all API operations. Implemented as a partial class to organize API methods into logical groups:
ApiChecks- HTTP monitoring checksApiRecipients- Notification recipientsApiStatusPages- Public status pagesApiPulse- Heartbeat monitoringApiDowntimes- Downtime historyApiMetrics- Performance metricsApiNodes- Monitoring node information
Base class providing:
- HTTP request/response handling
- JSON serialization/deserialization
- Error handling and exception mapping
- Common HTTP method wrappers (GET, POST, PUT, DELETE)
Factory class for creating UpdownClient instances:
- Static methods for simple creation
- Builder pattern support for advanced configuration
- Handles HttpClient lifecycle recommendations
Fluent builder for configuring clients:
- API key configuration
- Custom HttpClient injection
- Base address customization
- Timeout configuration
- User agent customization
HttpClient should be reused rather than created per request to avoid socket exhaustion. However, the static singleton pattern has thread-safety issues when mutating headers.
// In ASP.NET Core Startup/Program.cs
services.AddHttpClient<IUpdownClient, UpdownClient>((serviceProvider, client) =>
{
client.BaseAddress = new Uri("https://updown.io");
client.DefaultRequestHeaders.Add("X-API-KEY", "your-api-key");
});var client = UpdownClientFactory.CreateBuilder()
.WithApiKey("your-api-key")
.WithTimeout(TimeSpan.FromSeconds(30))
.Build();var client = UpdownClientFactory.Create("your-api-key");Exception
└── UpdownApiException (base for all API errors)
├── UpdownNotFoundException (404)
├── UpdownUnauthorizedException (401/403)
├── UpdownBadRequestException (400)
└── UpdownRateLimitException (429)
- HTTP Request → Made via HttpClient
- Response Check →
HandleErrorResponseAsyncexamines status code - Exception Creation → Specific exception type created based on status
- Error Details → Response content captured for debugging
- Throw → Exception thrown to caller
try
{
var check = await client.CheckAsync("token");
}
catch (UpdownNotFoundException ex)
{
// Handle 404 - check doesn't exist
Console.WriteLine($"Check not found: {ex.Message}");
}
catch (UpdownUnauthorizedException ex)
{
// Handle authentication failure
Console.WriteLine($"Auth failed: {ex.Message}");
}
catch (UpdownRateLimitException ex)
{
// Handle rate limiting
Console.WriteLine($"Rate limited. Retry after {ex.RetryAfterSeconds} seconds");
if (ex.ResetTime.HasValue)
{
await Task.Delay(TimeSpan.FromSeconds(ex.RetryAfterSeconds ?? 60));
// Retry request...
}
}
catch (UpdownApiException ex)
{
// Handle other API errors
Console.WriteLine($"API error ({ex.StatusCode}): {ex.Message}");
Console.WriteLine($"Response: {ex.ResponseContent}");
}All API methods are fully asynchronous:
- Use
ConfigureAwait(false)to avoid deadlocks - Support
CancellationTokenfor request cancellation - Properly dispose resources in async methods
UpdownClientinstances are thread-safe for concurrent requests- HttpClient is designed to be reused across threads
- No shared mutable state in the client
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
var checks = await client.ChecksAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Request cancelled after 10 seconds");
}Uses System.Text.Json with the following options:
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull- Ignores null values when serializing requests
- Gracefully handles missing properties when deserializing
- Uses
JsonPropertyNameattributes for API field mapping - Maintains C# naming conventions (PascalCase) in models
- Maps to API conventions (snake_case) via attributes
Example:
[JsonPropertyName("last_check_at")]
public DateTimeOffset? LastCheckAt { get; set; }You can inject custom HttpClientHandler or DelegatingHandler instances:
var handler = new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(5),
AutomaticDecompression = DecompressionMethods.All
};
var httpClient = new HttpClient(handler)
{
BaseAddress = new Uri("https://updown.io")
};
httpClient.DefaultRequestHeaders.Add("X-API-KEY", "your-key");
var client = UpdownClientFactory.Create(httpClient);Override HandleErrorResponseAsync in a derived class:
public class CustomUpdownClient : UpdownClient
{
public CustomUpdownClient(HttpClient httpClient) : base(httpClient) { }
protected override async Task HandleErrorResponseAsync(
HttpResponseMessage response,
CancellationToken cancellationToken)
{
// Custom error logging
_logger.LogError($"API error: {response.StatusCode}");
// Call base implementation
await base.HandleErrorResponseAsync(response, cancellationToken);
}
}Models use get/set properties (mutable) for:
- Deserialization from API responses
- Easy property setting for request parameters
- Backward compatibility
Future versions may introduce record types for immutable response models.
All properties are properly annotated:
- Required properties: non-nullable types
- Optional properties: nullable types
- Helps prevent null reference exceptions at compile time
Old property names are marked as [Obsolete] and delegate to new properties:
[JsonPropertyName("last_status")]
public double? LastStatus { get; set; }
[Obsolete("Use LastStatus instead.")]
[JsonIgnore]
public double? Last_Status
{
get => LastStatus;
set => LastStatus = value;
}- Uses
SocketsHttpHandleron .NET 5+ for connection pooling - Configures
PooledConnectionLifetimeto prevent DNS issues - Enables automatic decompression for reduced bandwidth
- Uses
ReadAsStreamAsyncfor JSON deserialization (avoids string allocation) - Configures JSON serializer options once and reuses
- Minimal allocations per request
- Uses
ValueTaskwhere appropriate (future enhancement) - Avoids synchronous blocking calls
- Properly implements async methods throughout the stack
- Mock HTTP responses using WireMock.NET
- Test each API endpoint independently
- Verify request formatting and response parsing
- Test error handling paths
- Optional tests marked with
[Category("Integration")] - Run against real Updown.io API
- Require valid API key
- Should be run sparingly to avoid API rate limits
- Retry Policies - Automatic retry with exponential backoff using Polly
- Rate Limiting - Client-side rate limiting to prevent 429 errors
- Caching - Optional caching layer for frequently accessed data
- Webhooks - Support for receiving Updown.io webhook notifications
- Reactive Extensions - IObservable-based API for real-time monitoring
- Source Generators - Compile-time code generation for improved performance
- Migration to record types for response models
- IAsyncEnumerable support for paginated results
- Removal of obsolete methods and properties
- Stricter nullability annotations
The library has minimal external dependencies:
System.Text.Json(only for .NET 6.0 and netstandard2.0)- .NET 8.0+ includes this in the framework
Follows semantic versioning (SemVer):
- MAJOR: Breaking changes
- MINOR: New features, backward-compatible
- PATCH: Bug fixes, backward-compatible
- Never log API keys
- Store keys in secure configuration (Azure Key Vault, AWS Secrets Manager, etc.)
- Use environment variables or user secrets in development
- Rotate keys regularly
- All communication over HTTPS
- Certificate validation enabled by default
- No support for HTTP (by design)
- Avoid logging full API responses (may contain sensitive check URLs)
- Sanitize errors before presenting to end users
- Use structured logging with appropriate log levels