ASP.NET Core cheat sheet
Program.cs, minimal APIs, DI lifetimes, middleware order, config and auth.
Program.cs skeleton
var builder = WebApplication.CreateBuilder(args);
// ---- services ----
builder.Services.AddDbContext<ShopDb>(o => o.UseNpgsql(builder.Configuration.GetConnectionString("Shop")));
builder.Services.AddScoped<OrderService>();
builder.Services.AddOptions<SmtpOptions>().BindConfiguration("Smtp").ValidateOnStart();
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddHealthChecks();
builder.Services.AddRateLimiter(o => o.AddFixedWindowLimiter("api", w => { w.PermitLimit = 100; w.Window = TimeSpan.FromMinutes(1); }));
var app = builder.Build();
// ---- pipeline (order matters) ----
app.UseExceptionHandler();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCors("frontend");
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter();
// ---- endpoints ----
app.MapOpenApi();
app.MapHealthChecks("/health");
app.MapOrderEndpoints();
app.Run();Minimal API endpoints
var orders = app.MapGroup("/api/orders").RequireAuthorization().WithTags("Orders");
orders.MapGet("/", async (int? page, ShopDb db, CancellationToken ct) => await db.Orders.Take(20).ToListAsync(ct));
orders.MapGet("/{id:int}", async Task<Results<Ok<OrderDto>, NotFound>> (int id, ShopDb db) =>
await db.Orders.FindAsync(id) is { } o ? TypedResults.Ok(o.ToDto()) : TypedResults.NotFound());
orders.MapPost("/", async (CreateOrder req, OrderService svc) =>
{
var id = await svc.PlaceAsync(req);
return TypedResults.Created($"/api/orders/{id}", new { id });
});
orders.MapDelete("/{id:int}", DeleteOrder).RequireAuthorization("AdminOnly");Binding: route {id} β parameter, simple types β query string, complex types β JSON body, registered types β DI, ClaimsPrincipal, HttpContext, CancellationToken.
DI lifetimes
| Register | Lifetime | Use for |
|---|---|---|
AddSingleton<T>() | One per app | Stateless/thread-safe services, caches |
AddScoped<T>() | One per request | DbContext, unit of work |
AddTransient<T>() | New each time | Lightweight stateless helpers |
Never inject a scoped service into a singleton.
Configuration
string host = builder.Configuration["Smtp:Host"]!; // appsettings.json β env vars (Smtp__Host)
public class OrderService(IOptions<SmtpOptions> smtp, ILogger<OrderService> log) { ... }Precedence: appsettings.json < appsettings.{Environment}.json < user secrets (dev) < environment variables < command line.
Middleware
app.Use(async (ctx, next) =>
{
// before
await next(ctx);
// after
});
app.UseMiddleware<RequestTimingMiddleware>();Auth
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => { o.Authority = "https://login.example.com"; o.Audience = "shop-api"; });
builder.Services.AddAuthorizationBuilder()
.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
app.MapGet("/me", (ClaimsPrincipal user) => user.FindFirstValue(ClaimTypes.NameIdentifier)).RequireAuthorization();401 = not authenticated, 403 = not allowed.
Logging
logger.LogInformation("Order {OrderId} placed by {UserId}", order.Id, userId); // structured
logger.LogError(ex, "Payment failed for {OrderId}", order.Id);Testing
public class Api(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
[Fact] public async Task Health_IsOk() =>
Assert.Equal(HttpStatusCode.OK, (await factory.CreateClient().GetAsync("/health")).StatusCode);
}
// Program.cs: public partial class Program;