Feb 11 2026
/metrics/metrics
/health/live/health/ready
billing_jobs_processed_total
dotnet add OrderManagement.Api package AspNetCore.HealthChecks.NpgSql
dotnet add OrderManagement.Api package prometheus-net.AspNetCore
AspNetCore.HealthChecks.NpgSql gives us a ready-made PostgreSQL health probe.prometheus-net.AspNetCore exposes /metrics and HTTP request metrics.
var postgres = builder.Configuration.GetConnectionString("Postgres")
?? "Host=localhost;Port=5432;Database=orders;Username=postgres;Password=postgres";
// Health checks
builder.Services.AddHealthChecks()
// Liveness: “process is alive”
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
// Readiness: “dependencies are reachable”
.AddNpgSql(postgres, name: "postgres", tags: new[] { "ready" });
var app = builder.Build();
app.UseHttpMetrics();
app.MapMetrics("/metrics");
app.MapHealthChecks("/health/live", new HealthCheckOptions {
Predicate = r => r.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions {
Predicate = r => r.Tags.Contains("ready")
});
app.MapGet("/api/ping", () => Results.Ok (new { ok = true, at = DateTimeOffset.UtcNow }));
/health/live checks only the app itself./health/ready checks PostgreSQL connectivity./metrics exposes Prometheus-format metrics.UseHttpMetrics() auto-collects request metrics.
/health/*/metrics
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Prometheus;
namespace OrderManagement.Billing.Worker;
public partial class Program
{
private static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
var postgres = builder.Configuration.GetConnectionString("Postgres")
?? "Host=localhost;Port=5432;Database=appdb;Username=app;Password=app";
// Health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" })
.AddNpgSql(postgres, name: "postgres", tags: ["ready"]);
// Background job runner
builder.Services.AddHostedService<BillingJobRunner>();
var app = builder.Build();
// Metrics
app.UseHttpMetrics();
app.MapMetrics("/metrics");
// Health endpoints
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("live")
});
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains("ready")
});
app.Run();
}
// Custom business metric: how many jobs were processed
public static readonly Counter JobsProcessed = Metrics.CreateCounter(
"billing_jobs_processed_total",
"Total number of billing jobs processed.");
internal sealed class BillingJobRunner : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Simulate “job processed”
JobsProcessed.Inc();
await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken);
}
}
}
}
/metrics and health endpoints.billing_jobs_processed_total is a custom metric you’ll graph in Grafana.
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["OrderManagement.Api/OrderManagement.Api.csproj", "OrderManagement.Api/"]
RUN dotnet restore "./OrderManagement.Api/OrderManagement.Api.csproj"
COPY . .
WORKDIR "/src/OrderManagement.Api"
RUN dotnet build "./OrderManagement.Api.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./OrderManagement.Api.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderManagement.Api.dll"]
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["OrderManagement.Billing.Worker/OrderManagement.Billing.Worker.csproj", "OrderManagement.Billing.Worker/"]
RUN dotnet restore "./OrderManagement.Billing.Worker/OrderManagement.Billing.Worker.csproj"
COPY . .
WORKDIR "/src/OrderManagement.Billing.Worker"
RUN dotnet build "./OrderManagement.Billing.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./OrderManagement.Billing.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "OrderManagement.Billing.Worker.dll"]
docker-compose.yml in the solution root:services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: orders
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5433:5433"
orders-api:
build:
context: .
dockerfile: OrderManagement.Api/Dockerfile
environment:
ConnectionStrings__Postgres:
Host=postgres;Port=5433;Database=orders;Username=postgres;Password=postgres
ports:
- "8082:8080"
depends_on:
- postgres
billing-worker:
build:
context: .
dockerfile: OrderManagement.Billing.Worker/Dockerfile
environment:
ConnectionStrings__Postgres:
Host=postgres;Port=5433;Database=orders;Username=postgres;Password=postgres
ports:
- "8081:8080"
depends_on:
- postgres
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
ports:
- "9090:9090"
depends_on:
- orders-api
- billing-worker
grafana:
image: grafana/grafana:latest
volumes:
- ./ops/grafana/provisioning:/etc/grafana/provisioning:ro
- ./ops/grafana/dashboards:/var/lib/grafana/dashboards:ro
ports:
- "3003:3000"
depends_on:
- prometheus

ops/prometheus/prometheus.yml in your root folder of the solution:
global:
scrape_interval: 5s
scrape_configs:
- job_name: "orders-api"
metrics_path: /metrics
static_configs:
- targets:
- "orders-api:8080"
- job_name: "billing-worker"
metrics_path: /metrics
static_configs:
- targets:
- "billing-worker:8080"
ops/grafana/provisioning/datasources/datasource.yml
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
docker compose up --build
http://localhost:8082/health/livehttp://localhost:8082/health/readyhttp://localhost:8081/health/ready
http://localhost:8082/metricshttp://localhost:8081/metrics
http_requests_received_totalbilling_jobs_processed_total
http://localhost:9090
http://localhost:3000 (default login admin/admin)
http://prometheus:9090
rate(http_requests_received_total{job="orders-api"}[1m])
billing_jobs_processed_total
rate(billing_jobs_processed_total[1m]) * 60

1. Design Patterns that Deliver
This isn’t just another design patterns book. Dive into real-world examples and practical solutions to real problems in real applications.Check out it here.
Go-to resource for understanding the core concepts of design patterns without the overwhelming complexity. In this concise and affordable ebook, I've distilled the essence of design patterns into an easy-to-digest format. It is a Beginner level. Check out it here.
Every Monday morning, I share 1 actionable tip on C#, .NET & Arcitecture topic, that you can use right away.
Join 18,000+ subscribers to improve your .NET Knowledge.
Subscribe to the TheCodeMan.net and be among the 18,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.