Your ASP.NET Core app is deployed. The CI pipeline is green. But is it actually serving requests right now? Is the background service that processes your payment queue still running? Will you know within a minute if the database connection pool exhausts at midnight?
This tutorial shows you how to integrate production-grade uptime monitoring into an ASP.NET Core application using Vigilmon. We cover:
- The built-in ASP.NET Core Health Checks middleware wired to a
/healthendpoint - Vigilmon webhook integration for instant alert delivery
- A background service heartbeat pattern so silent worker failures trigger alerts
Prerequisites
- .NET 8 or later
- An existing ASP.NET Core Web API or MVC project
- A free account at vigilmon.online
Part 1: Add health checks middleware
ASP.NET Core ships with a first-class health checks system in Microsoft.Extensions.Diagnostics.HealthChecks. You don't need a third-party library to expose a standards-compliant /health endpoint.
Basic registration
In Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Register health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy("API is running"))
.AddSqlServer(
connectionString: builder.Configuration.GetConnectionString("Default")!,
name: "database",
failureStatus: HealthStatus.Unhealthy,
tags: ["db", "sql"]
);
var app = builder.Build();
// Map the health endpoint
app.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = WriteJsonResponseAsync,
ResultStatusCodes =
{
[HealthStatus.Healthy] = StatusCodes.Status200OK,
[HealthStatus.Degraded] = StatusCodes.Status200OK,
[HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable,
}
});
app.Run();
The AddSqlServer extension lives in AspNetCore.HealthChecks.SqlServer. Install it:
dotnet add package AspNetCore.HealthChecks.SqlServer
JSON response writer
By default the health endpoint returns plain text (Healthy). Replace it with a structured JSON response that Vigilmon and your dashboards can parse:
static Task WriteJsonResponseAsync(HttpContext context, HealthReport report)
{
context.Response.ContentType = "application/json";
var result = new
{
status = report.Status.ToString(),
timestamp = DateTimeOffset.UtcNow,
duration = report.TotalDuration,
checks = report.Entries.Select(e => new
{
name = e.Key,
status = e.Value.Status.ToString(),
description = e.Value.Description,
duration = e.Value.Duration,
})
};
return context.Response.WriteAsync(
JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = false })
);
}
Test it:
curl https://localhost:5001/health
{
"status": "Healthy",
"timestamp": "2026-06-29T07:00:00+00:00",
"duration": "00:00:00.0123456",
"checks": [
{ "name": "self", "status": "Healthy", "description": "API is running", "duration": "00:00:00.0001" },
{ "name": "database", "status": "Healthy", "description": null, "duration": "00:00:00.0112" }
]
}
HTTP 503 is returned automatically when any check is Unhealthy, which Vigilmon treats as a monitor failure.
Custom health check: Redis
public class RedisHealthCheck : IHealthCheck
{
private readonly IConnectionMultiplexer _redis;
public RedisHealthCheck(IConnectionMultiplexer redis) => _redis = redis;
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var db = _redis.GetDatabase();
await db.PingAsync();
return HealthCheckResult.Healthy("Redis is reachable");
}
catch (Exception ex)
{
return HealthCheckResult.Unhealthy("Redis ping failed", ex);
}
}
}
Register it:
builder.Services.AddHealthChecks()
.AddCheck<RedisHealthCheck>("redis");
Part 2: Configure HTTP monitoring in Vigilmon
- Log in to vigilmon.online and click Add Monitor.
- Choose HTTP(S) monitor.
- Enter your health URL:
https://yourapp.example.com/health. - Set the check interval to 1 minute.
- Add your email or Slack address as the alert destination.
- Click Save.
Vigilmon will ping /health every minute. A non-2xx response or timeout triggers an alert.
Exclude /health from authentication middleware
If your app uses JWT bearer or cookie auth globally, ensure the health endpoint is accessible without credentials:
app.MapHealthChecks("/health")
.AllowAnonymous();
Part 3: Receive Vigilmon webhooks
For richer alerting — paging on-call via PagerDuty, posting to a Teams channel, or updating an internal status page — configure a webhook alert and receive it in a minimal controller:
// VigilmonWebhookController.cs
[ApiController]
[Route("webhook/vigilmon")]
public class VigilmonWebhookController : ControllerBase
{
private readonly ILogger<VigilmonWebhookController> _logger;
public VigilmonWebhookController(ILogger<VigilmonWebhookController> logger)
=> _logger = logger;
[HttpPost]
public IActionResult Receive([FromBody] VigilmonWebhookPayload payload)
{
if (payload.Status == "down")
{
_logger.LogCritical(
"Monitor {Monitor} is DOWN. URL: {Url}, HTTP: {Code}",
payload.MonitorName, payload.Url, payload.ResponseCode);
// Dispatch to PagerDuty, send email, update status page, etc.
}
return NoContent();
}
}
public record VigilmonWebhookPayload(
[property: JsonPropertyName("monitor_id")] string MonitorId,
[property: JsonPropertyName("monitor_name")] string MonitorName,
[property: JsonPropertyName("status")] string Status,
[property: JsonPropertyName("url")] string Url,
[property: JsonPropertyName("checked_at")] DateTimeOffset CheckedAt,
[property: JsonPropertyName("response_code")] int? ResponseCode,
[property: JsonPropertyName("response_time_ms")] int? ResponseTimeMs
);
Vigilmon sends this payload on every DOWN and UP transition, giving you a clean event stream to act on.
Part 4: Background service heartbeat pattern
The health endpoint proves the web tier is alive, but it says nothing about background workers. A BackgroundService that silently crashes — or gets stuck in a deadlock — will not affect the HTTP 200 from /health.
The heartbeat pattern solves this: your worker periodically pings a Vigilmon heartbeat URL. If the pings stop arriving, Vigilmon fires an alert.
Create the Vigilmon heartbeat monitor
- In Vigilmon, click Add Monitor.
- Choose Heartbeat monitor.
- Set the expected interval to 5 minutes.
- Copy the unique URL:
https://vigilmon.online/heartbeat/abc123xyz
Implement the heartbeat in a BackgroundService
// PaymentProcessorService.cs
public class PaymentProcessorService : BackgroundService
{
private readonly IHttpClientFactory _httpFactory;
private readonly ILogger<PaymentProcessorService> _logger;
private readonly string _heartbeatUrl;
public PaymentProcessorService(
IHttpClientFactory httpFactory,
IConfiguration config,
ILogger<PaymentProcessorService> logger)
{
_httpFactory = httpFactory;
_logger = logger;
_heartbeatUrl = config["Vigilmon:HeartbeatUrl"]
?? throw new InvalidOperationException("Vigilmon:HeartbeatUrl not configured");
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await ProcessPendingPaymentsAsync(stoppingToken);
await PingHeartbeatAsync(stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Payment processing cycle failed");
// Don't ping heartbeat on failure — let Vigilmon alert
}
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
}
}
private async Task ProcessPendingPaymentsAsync(CancellationToken ct)
{
// ... your actual processing logic
_logger.LogInformation("Processing payments cycle complete");
}
private async Task PingHeartbeatAsync(CancellationToken ct)
{
try
{
using var client = _httpFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
await client.GetAsync(_heartbeatUrl, ct);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Vigilmon heartbeat ping failed");
}
}
}
Register the service and configure the URL:
// Program.cs
builder.Services.AddHttpClient();
builder.Services.AddHostedService<PaymentProcessorService>();
// appsettings.json
{
"Vigilmon": {
"HeartbeatUrl": "https://vigilmon.online/heartbeat/abc123xyz"
}
}
Now if PaymentProcessorService crashes, deadlocks, or the process is killed, Vigilmon will not receive the expected heartbeat and will fire an alert within one interval window (5 minutes in this example).
Typed HttpClient variant
For cleaner dependency injection, register a typed client:
builder.Services.AddHttpClient<VigilmonHeartbeatClient>(client =>
{
client.BaseAddress = new Uri("https://vigilmon.online/");
client.Timeout = TimeSpan.FromSeconds(10);
});
public class VigilmonHeartbeatClient
{
private readonly HttpClient _http;
private readonly string _path;
public VigilmonHeartbeatClient(HttpClient http, IConfiguration config)
{
_http = http;
_path = config["Vigilmon:HeartbeatPath"]!;
}
public Task PingAsync(CancellationToken ct = default)
=> _http.GetAsync(_path, ct);
}
Part 5: Multi-environment configuration
Use ASP.NET Core's environment-specific config files to avoid pinging a shared Vigilmon monitor from staging:
// appsettings.Development.json
{
"Vigilmon": {
"HeartbeatUrl": ""
}
}
In your service, guard with a null/empty check:
if (!string.IsNullOrWhiteSpace(_heartbeatUrl))
{
await PingHeartbeatAsync(ct);
}
This ensures development runs never dirty your production heartbeat data.
Summary
Your ASP.NET Core application now has three layers of production observability:
/healthendpoint — combines ASP.NET Core Health Checks with a JSON response writer; returns HTTP 503 when any dependency is unhealthy.- Webhook receiver — a minimal controller endpoint that accepts Vigilmon DOWN/UP events and can fan them out to PagerDuty, Teams, email, or your own status page.
- Background service heartbeat — a
BackgroundServicepings a Vigilmon heartbeat URL on each successful processing cycle; silence = alert.
Vigilmon handles the scheduling, history, and alert routing. You own the health logic that knows what "healthy" means for your application.
Monitor your .NET app free at vigilmon.online
#dotnet #csharp #aspnet #monitoring