The web recipes every project needs sooner or later: accepting file uploads, protecting endpoints with JWT, letting the browser call you (CORS), caching responses, running work in the background, and telling the load balancer you're alive. Basics first? Start with the Minimal API guide and REST Best Practices. For async messaging see MassTransit; for Azure Table Storage, Service Bus and Functions see Azure for .NET In Depth.
How To
Accept file uploads — safely
Three non-negotiables: cap the size, allowlist the extensions, and never use the client's file name (it can contain ../../ path tricks). Generate your own name; keep uploads outside wwwroot.
app.MapPost("/api/upload",
async Results<Ok<string>, BadRequest<string>> (IFormFile file) =>
{
const long MaxBytes = 5 * 1024 * 1024; // 5 MB cap
string[] allowed = [".jpg", ".png", ".pdf"]; // extension allowlist
if (file.Length == 0 || file.Length > MaxBytes)
return TypedResults.BadRequest("File must be 1 byte – 5 MB.");
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!allowed.Contains(ext))
return TypedResults.BadRequest($"Only {string.Join(", ", allowed)} allowed.");
// NEVER trust file.FileName for the saved name — generate your own
var safeName = $"{Guid.NewGuid()}{ext}";
var folder = Path.Combine(AppContext.BaseDirectory, "uploads");
Directory.CreateDirectory(folder);
await using var stream = File.Create(Path.Combine(folder, safeName));
await file.CopyToAsync(stream);
return TypedResults.Ok(safeName);
}).DisableAntiforgery(); // .NET 8+: form endpoints need this (or a real token)
// test it:
// curl -F "file=@photo.jpg" http://localhost:5088/api/upload
How To
Protect endpoints with JWT — the minimal recipe
Package: Microsoft.AspNetCore.Authentication.JwtBearer. Three parts: validate incoming tokens, issue tokens at login, protect endpoints. Keep the key in user-secrets/env vars — never in source.
// 1. VALIDATE — Program.cs
var key = builder.Configuration["Jwt:Key"]!; // dotnet user-secrets set "Jwt:Key" "..."
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => o.TokenValidationParameters = new()
{
ValidIssuer = "myapp",
ValidAudience = "myapp-clients",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
ValidateIssuerSigningKey = true,
});
builder.Services.AddAuthorization();
// pipeline (order!):
app.UseAuthentication();
app.UseAuthorization();
// 2. ISSUE — login endpoint returns a token
app.MapPost("/api/login", (LoginDto login) =>
{
// ...verify credentials against your user store first!...
var token = new JwtSecurityToken(
issuer: "myapp", audience: "myapp-clients",
claims: [ new Claim(ClaimTypes.Name, login.Username),
new Claim(ClaimTypes.Role, "Admin") ],
expires: DateTime.UtcNow.AddMinutes(30), // SHORT expiry
signingCredentials: new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)),
SecurityAlgorithms.HmacSha256));
return Results.Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
});
// 3. PROTECT
app.MapDelete("/api/products/{id}", ...).RequireAuthorization();
app.MapGet("/api/admin/stats", ...).RequireAuthorization(p => p.RequireRole("Admin"));
// client sends: Authorization: Bearer eyJhbGciOi...
How To
Enable CORS — let your frontend call your API
Browser says "blocked by CORS policy"? The browser blocks cross-origin calls unless your API explicitly allows that origin. Name exact origins — AllowAnyOrigin in production is an open door.
// Program.cs
const string Frontend = "frontend";
builder.Services.AddCors(options =>
options.AddPolicy(Frontend, policy => policy
.WithOrigins("http://localhost:5173", // dev (Vite/React)
"https://myapp.com") // production
.AllowAnyHeader()
.AllowAnyMethod()));
var app = builder.Build();
app.UseCors(Frontend); // BEFORE UseAuthentication / endpoints
// or per endpoint-group:
app.MapGroup("/api/public").RequireCors(Frontend);
// Debug checklist when it still fails:
// 1. Origin must match EXACTLY (scheme + host + port — no trailing slash)
// 2. The error is only in the BROWSER — curl/Postman never hit CORS
// 3. Preflight = the OPTIONS request you see before POST/PUT
// 4. Credentials (cookies)? add .AllowCredentials() — and then
// AllowAnyOrigin is forbidden by the spec
How To
Cache responses — output caching (.NET 8+)
If the product list changes every few minutes but is requested every few milliseconds, don't hit the database every time. Output caching stores the rendered response and replays it.
// Program.cs
builder.Services.AddOutputCache(options =>
{
// a named policy: 60s lifetime, vary by the query string
options.AddPolicy("Products", p => p
.Expire(TimeSpan.FromSeconds(60))
.SetVaryByQuery("page", "pageSize", "search"));
});
var app = builder.Build();
app.UseOutputCache(); // after UseCors, before endpoints
// apply per endpoint…
app.MapGet("/api/products", GetProducts).CacheOutput("Products");
// …and evict when data changes:
app.MapPost("/api/products",
async (CreateProductDto dto, IOutputCacheStore cache, CancellationToken ct) =>
{
var created = await CreateAsync(dto);
await cache.EvictByTagAsync("products", ct); // pair with .Tag("products")
return TypedResults.Created($"/api/products/{created.Id}", created);
});
// Rule of thumb: cache GETs that are expensive + frequently read
// + tolerably stale. Never cache per-user data without VaryByHeader.
How To
Run background work — BackgroundService + PeriodicTimer
Cleanup jobs, email queues, sync tasks — anything that runs on a schedule inside your API process. PeriodicTimer is the modern loop; scoped services must be resolved per tick. For persistent jobs, retries, dashboard and cron, see Hangfire & Quartz In Depth.
public class CleanupWorker(IServiceScopeFactory scopes,
ILogger<CleanupWorker> log) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(15));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
// BackgroundService is a singleton — DbContext is scoped.
// Create a scope per tick to resolve scoped services:
using var scope = scopes.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var removed = await db.Sessions
.Where(s => s.ExpiresAt < DateTime.UtcNow)
.ExecuteDeleteAsync(stoppingToken); // EF 7+ bulk delete
log.LogInformation("Cleanup removed {Count} sessions", removed);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
log.LogError(ex, "Cleanup tick failed"); // log & keep looping
}
}
}
}
// Program.cs
builder.Services.AddHostedService<CleanupWorker>();
How To
Add health checks — tell the world you're alive
Load balancers, Kubernetes and uptime monitors all ask the same question: "are you OK?" Give them an endpoint that actually checks your dependencies, not just returns 200.
// Program.cs
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
// packages exist for everything: AspNetCore.HealthChecks.SqlServer, .Redis, …
.AddSqlServer(builder.Configuration.GetConnectionString("Default")!,
name: "database", failureStatus: HealthStatus.Unhealthy);
var app = builder.Build();
app.MapHealthChecks("/health/live", new() // "is the process up?"
{
Predicate = r => r.Name == "self"
});
app.MapHealthChecks("/health/ready", new() // "can I serve traffic?" (checks DB too)
{
Predicate = _ => true
});
// GET /health/ready → 200 "Healthy" | 503 "Unhealthy"
//
// Kubernetes wiring:
// livenessProbe: httpGet: { path: /health/live, port: 8080 }
// readinessProbe: httpGet: { path: /health/ready, port: 8080 }