Performance profiling is the process of measuring where time and memory are actually spent in an application — not where developers assume. Assumptions about bottlenecks are frequently wrong: the imagined slow endpoint is fast; the overlooked database query is catastrophically slow. The tools in this lesson — dotnet-trace, dotnet-counters, EF Core query logging, and load testing with k6 — provide objective measurements that drive targeted optimisation rather than guesswork. OpenTelemetry is the standard for distributed tracing across microservices.
Diagnostics and Profiling Tools
// ── OpenTelemetry — distributed tracing ────────────────────────────────────
// dotnet add package OpenTelemetry.Extensions.Hosting
// dotnet add package OpenTelemetry.Instrumentation.AspNetCore
// dotnet add package OpenTelemetry.Instrumentation.EntityFrameworkCore
// dotnet add package OpenTelemetry.Exporter.Console (dev) or .Otlp (prod)
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing
.AddAspNetCoreInstrumentation(opts =>
{
opts.RecordException = true;
opts.Filter = ctx => ctx.Request.Path != "/health";
})
.AddEntityFrameworkCoreInstrumentation(opts =>
{
opts.SetDbStatementForText = true; // capture SQL in traces (dev only)
opts.SetDbStatementForStoredProcedure = true;
})
.AddHttpClientInstrumentation()
.SetSampler(new TraceIdRatioBasedSampler(0.1)) // sample 10% in prod
.AddConsoleExporter() // dev: console
// .AddOtlpExporter() // prod: Jaeger, Zipkin, or Application Insights
;
})
.WithMetrics(metrics =>
{
metrics
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation() // GC, thread pool, etc.
.AddPrometheusExporter(); // expose /metrics endpoint
});
app.MapPrometheusScrapingEndpoint("/metrics"); // Prometheus scrapes this
// ── EF Core slow query detection ──────────────────────────────────────────
builder.Services.AddDbContext<AppDbContext>(opts =>
{
opts.UseSqlServer(connectionString);
if (app.Environment.IsDevelopment())
{
opts.EnableSensitiveDataLogging(); // log parameter values
opts.EnableDetailedErrors();
opts.LogTo(sql =>
{
if (sql.Contains("SLOW QUERY")) // detect from EF Core event
logger.LogWarning("Slow EF Core query: {Sql}", sql);
}, LogLevel.Information);
}
});
dotnet-counters monitor --process-id {pid} --counters System.Runtime,Microsoft.AspNetCore.Hosting,Microsoft.EntityFrameworkCore on a live process for real-time performance counters: requests per second, active requests, GC pressure, thread pool queue depth, and EF Core query counts. This zero-restart, zero-code-change diagnostic tool is the fastest way to verify the health of a deployed API. Elevated Gen2 GC counts indicate large object allocations (often LOH fragmentation from large buffers or string allocations).EnableSensitiveDataLogging() logs SQL parameter values — including user IDs, email addresses, and potentially passwords in queries. This is invaluable in development for debugging EF Core queries but must never be enabled in production. A production log with sensitive data logged in SQL parameters violates GDPR, PCI-DSS, and most data protection regulations. Gate it behind if (app.Environment.IsDevelopment()) and verify that production builds have it disabled via an integration test that checks the EF Core options.Load Testing with k6
// ── k6 load test script (JavaScript) — reference for API validation ───────
// import http from 'k6/http';
// import { check, sleep } from 'k6';
//
// export const options = {
// stages: [
// { duration: '30s', target: 50 }, // ramp up to 50 users
// { duration: '1m', target: 50 }, // hold at 50 users
// { duration: '10s', target: 0 }, // ramp down
// ],
// thresholds: {
// http_req_duration: ['p(95)<200'], // 95% of requests under 200ms
// http_req_failed: ['rate<0.01'], // less than 1% error rate
// },
// };
//
// export default function () {
// const res = http.get('https://localhost:5001/api/posts?page=1&size=10');
// check(res, { 'status is 200': (r) => r.status === 200 });
// sleep(1);
// }
//
// Run: k6 run load-test.js
// Output: req/s, p95 latency, error rate — compare against thresholds
// ── Performance anti-patterns to eliminate ────────────────────────────────
// ❌ N+1 queries: foreach (post) { await GetCommentsAsync(post.Id) }
// ❌ Missing AsNoTracking() on read-only queries
// ❌ Loading full entities when only 2 fields needed (use Select/projection)
// ❌ Synchronous I/O anywhere in the request pipeline
// ❌ String concatenation in loops (use StringBuilder or interpolation with +)
// ❌ LINQ queries with client-side evaluation (EF Core warns but allows this)
Common Mistakes
Mistake 1 — Optimising without measuring (fixing the wrong bottleneck)
❌ Wrong — developer assumes the slow endpoint is the JSON serialiser; spends a week on serialiser tuning; actual bottleneck is a missing database index.
✅ Correct — measure first with OpenTelemetry traces and EF Core query logging; optimise the measured bottleneck.
Mistake 2 — Enabling sensitive data logging in production (GDPR violation)
❌ Wrong — opts.EnableSensitiveDataLogging() unconditionally; SQL parameters with email addresses in production logs.
✅ Correct — gate behind if (app.Environment.IsDevelopment()); verify it is off in production via test.