C# ASP.NET Core Cheat Sheet
Covers building ASP.NET Core Web APIs with controllers and minimal APIs, the middleware pipeline, routing, and model binding.
API Controller
A typical RESTful controller with attribute routing.
[ApiController][Route("api/[controller]")]public class ProductsController : ControllerBase{ private readonly IProductService _service; public ProductsController(IProductService service) => _service = service; [HttpGet] public async Task<ActionResult<List<Product>>> GetAll() => Ok(await _service.GetAllAsync()); [HttpGet("{id:int}")] public async Task<ActionResult<Product>> GetById(int id) { var product = await _service.GetByIdAsync(id); return product is null ? NotFound() : Ok(product); } [HttpPost] public async Task<ActionResult<Product>> Create(Product product) { await _service.AddAsync(product); return CreatedAtAction(nameof(GetById), new { id = product.Id }, product); }}
Middleware Pipeline
Built-in and custom request/response middleware.
var app = builder.Build();app.UseHttpsRedirection();app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.UseCors("AllowAll");app.MapControllers();app.Run();// Custom middlewareapp.Use(async (context, next) =>{ Console.WriteLine($"Request: {context.Request.Path}"); await next(context); // Call the next middleware in the pipeline});
Minimal API Endpoints
Defining routes without a full controller class.
var app = builder.Build();app.MapGet("/products/{id}", async (int id, IProductService svc) =>{ var p = await svc.GetByIdAsync(id); return p is not null ? Results.Ok(p) : Results.NotFound();});app.MapPost("/products", async (Product p, IProductService svc) =>{ await svc.AddAsync(p); return Results.Created($"/products/{p.Id}", p);});app.Run();
Core Building Blocks
Concepts that show up in every ASP.NET Core app.
- [ApiController]- Enables automatic model validation and binding-source inference for API controllers
- Routing- Attribute routing ([Route], [HttpGet]) or conventional routing via MapControllerRoute
- Model binding- Automatically maps route, query, and body values to action method parameters
- Middleware order- Executes top-to-bottom for requests and bottom-to-top for responses (a pipeline)
- Filters- [Authorize], action filters, and exception filters run around action execution
- IActionResult- Ok(), NotFound(), BadRequest(), CreatedAtAction() return the correct HTTP status codes
- Options pattern- services.Configure<T>(config.GetSection("Section")) binds config to a strongly-typed class
JWT Bearer Authentication
Configuring token-based authentication and protecting endpoints.
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]!)) }; });builder.Services.AddAuthorization(options =>{ options.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));});// On a controller or minimal API endpoint[Authorize(Policy = "AdminOnly")][HttpDelete("{id:int}")]public async Task<IActionResult> Delete(int id) => NoContent();
Dependency Injection Lifetimes
Choosing the correct service lifetime to avoid captive dependencies.
builder.Services.AddSingleton<ICacheService, MemoryCacheService>(); // One instance for app lifetimebuilder.Services.AddScoped<IOrderRepository, OrderRepository>(); // One instance per HTTP requestbuilder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // New instance every resolution// Anti-pattern: injecting a Scoped service into a Singleton captures it beyond its lifetime.// Fix with IServiceScopeFactory when a singleton needs scoped work (e.g. background service):public class ReportWorker : BackgroundService{ private readonly IServiceScopeFactory _scopeFactory; public ReportWorker(IServiceScopeFactory scopeFactory) => _scopeFactory = scopeFactory; protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using var scope = _scopeFactory.CreateScope(); var repo = scope.ServiceProvider.GetRequiredService<IOrderRepository>(); await repo.ProcessPendingAsync(stoppingToken); }}
Global Exception Handling Middleware
Centralizing error responses with ProblemDetails instead of try/catch in every action.
app.UseExceptionHandler(errorApp =>{ errorApp.Run(async context => { var feature = context.Features.Get<IExceptionHandlerFeature>(); var ex = feature?.Error; var problem = new ProblemDetails { Status = ex is ArgumentException ? StatusCodes.Status400BadRequest : StatusCodes.Status500InternalServerError, Title = ex is ArgumentException ? "Invalid request" : "An unexpected error occurred", Detail = app.Environment.IsDevelopment() ? ex?.ToString() : null }; context.Response.StatusCode = problem.Status ?? 500; context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsJsonAsync(problem); });});// .NET 8+ alternative: implement IExceptionHandler and register with// builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
Custom Action Filter
Cross-cutting logic (validation, logging, timing) that wraps action execution.
public class ValidateModelFilter : IActionFilter{ public void OnActionExecuting(ActionExecutingContext context) { if (!context.ModelState.IsValid) { context.Result = new BadRequestObjectResult(context.ModelState); } } public void OnActionExecuted(ActionExecutedContext context) { }}// Register globally so every controller gets itbuilder.Services.AddControllers(options =>{ options.Filters.Add<ValidateModelFilter>();});// Or scope it to one action[ServiceFilter(typeof(ValidateModelFilter))][HttpPost]public IActionResult Create(ProductDto dto) => Ok(dto);
API Versioning & Rate Limiting
Two production concerns rarely covered in intro tutorials.
builder.Services.AddApiVersioning(options =>{ options.DefaultApiVersion = new ApiVersion(1, 0); options.AssumeDefaultVersionWhenUnspecified = true; options.ReportApiVersions = true;});builder.Services.AddRateLimiter(options =>{ options.AddFixedWindowLimiter("fixed", opt => { opt.PermitLimit = 100; opt.Window = TimeSpan.FromMinutes(1); opt.QueueLimit = 10; opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;});var app = builder.Build();app.UseRateLimiter();app.MapGet("/products", () => Results.Ok()) .RequireRateLimiting("fixed") .MapToApiVersion(1, 0);
Production-Grade Concepts
Things a real deployment needs beyond a basic CRUD controller.
- IHostedService / BackgroundService- Long-running or scheduled work that runs alongside the web host (queue processors, cron-like jobs)
- Health checks- AddHealthChecks()/MapHealthChecks("/health") exposes liveness/readiness probes for orchestrators like Kubernetes
- IHttpClientFactory- Pools and reuses HttpMessageHandlers, avoiding socket exhaustion from manually newing up HttpClient
- Response caching / output caching- [ResponseCache] or AddOutputCache() reduces load for expensive, repeat-read endpoints
- Problem Details (RFC 7807)- Standardized machine-readable error payloads returned via AddProblemDetails()
- Configuration providers- appsettings.{Environment}.json, user secrets, and environment variables layer with precedence at runtime
- IOptionsMonitor<T>- Delivers live-reloaded configuration to singletons, unlike IOptions<T> which snapshots at startup
Register middleware order carefully — UseAuthentication() must come before UseAuthorization(), and both must come before MapControllers(), or auth checks silently won't apply.