ASP.NET Core
Overview
ASP.NET Core is Microsoft's cross-platform, high-performance web framework for building modern web APIs, web applications, and microservices. It runs on .NET 6+ and supports minimal APIs (lightweight endpoints), controller-based APIs, Razor Pages, MVC, Blazor, gRPC, and SignalR. ASP.NET Core uses a middleware pipeline for request processing, built-in dependency injection, configuration from multiple sources (JSON, environment variables, Azure Key Vault), and OpenAPI/Swagger integration for API documentation.
Minimal API Setup
Build lightweight HTTP APIs with minimal ceremony using the minimal API pattern.
using Microsoft.AspNetCore.Http.HttpResults;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddProblemDetails();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseExceptionHandler();
var products = app.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
products.MapGet("/", async (IProductRepository repo) =>
TypedResults.Ok(await repo.GetAllAsync()));
products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (
int id, IProductRepository repo) =>
{
var product = await repo.GetByIdAsync(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
});
products.MapPost("/", async Task<Created<Product>> (
CreateProductRequest request,
IProductRepository repo) =>
{
var product = new Product
{
Name = request.Name,
Price = request.Price,
Category = request.Category
};
await repo.AddAsync(product);
return TypedResults.Created($"/api/products/{product.Id}", product);
}).WithValidationFilter();
products.MapDelete("/{id:int}", async Task<Results<NoContent, NotFound>> (
int id, IProductRepository repo) =>
{
var deleted = await repo.DeleteAsync(id);
return deleted ? TypedResults.NoContent() : TypedResults.NotFound();
});
app.Run();
public record CreateProductRequest(string Name, decimal Price, string Category);
Middleware Pipeline
Configure cross-cutting concerns using the middleware pipeline.
using System.Diagnostics;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", config =>
{
config.PermitLimit = 100;
config.Window = TimeSpan.FromMinutes(1);
config.QueueLimit = 10;
});
});
builder.Services.AddResponseCompression();
builder.Services.AddOutputCache();
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
var app = builder.Build();
// Middleware order matters
app.UseExceptionHandler("/error");
app.UseResponseCompression();
app.UseHttpsRedirection();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
app.UseOutputCache();
app.MapHealthChecks("/health");
// Custom middleware
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
context.Response.OnStarting(() =>
{
context.Response.Headers["X-Response-Time"] = $"{stopwatch.ElapsedMilliseconds}ms";
return Task.CompletedTask;
});
await next(context);
});
app.Run();
Dependency Injection and Service Lifetime
Register services with the correct lifetime to avoid captive dependency and threading issues.
var builder = WebApplication.CreateBuilder(args);
// Singleton: shared across all requests (thread-safe required)
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
// Scoped: one instance per HTTP request
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
// Transient: new instance every time
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
// Named HttpClient with resilience
builder.Services.AddHttpClient<IExternalApiClient, ExternalApiClient>(client =>
{
client.BaseAddress = new Uri("https://api.external.com/");
client.DefaultRequestHeaders.Add("Accept", "application/json");
})
.AddStandardResilienceHandler();
// Options pattern
builder.Services.Configure<SmtpOptions>(
builder.Configuration.GetSection("Smtp"));
var app = builder.Build();
Error Handling and Problem Details
Return consistent error responses using the RFC 7807 Problem Details standard.
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions["traceId"] =
Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;
};
});
var app = builder.Build();
app.UseExceptionHandler(exceptionApp =>
{
exceptionApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
var problemDetails = exception switch
{
NotFoundException e => new ProblemDetails
{
Status = 404,
Title = "Resource not found",
Detail = e.Message
},
ValidationException e => new ProblemDetails
{
Status = 400,
Title = "Validation failed",
Detail = e.Message
},
_ => new ProblemDetails
{
Status = 500,
Title = "An unexpected error occurred"
}
};
context.Response.StatusCode = problemDetails.Status ?? 500;
await context.Response.WriteAsJsonAsync(problemDetails);
});
});
ASP.NET Core Application Models
| Feature |
Minimal APIs |
Controllers |
Razor Pages |
Blazor |
| Use case |
Microservices, simple APIs |
Full-featured APIs |
Server-rendered pages |
Interactive web UI |
| Routing |
Lambda-based |
Attribute/conventional |
Page-based |
Component-based |
| Model binding |
Parameter injection |
[FromBody], [FromQuery] |
[BindProperty] |
@bind |
| Filters |
Endpoint filters |
Action filters |
Page filters |
N/A |
| OpenAPI |
Built-in |
Swashbuckle/NSwag |
N/A |
N/A |
| Overhead |
Minimal |
Moderate |
Moderate |
Higher |
Best Practices
Use TypedResults return types (e.g., Task<Results<Ok<T>, NotFound>>) on minimal API endpoints instead of returning IResult so that the OpenAPI generator infers response schemas and status codes automatically, producing accurate Swagger documentation without manual [ProducesResponseType] attributes.
Register DbContext as Scoped (the default for AddDbContext) and never inject it into Singleton services because DbContext is not thread-safe; injecting a scoped service into a singleton creates a captive dependency that shares a single DbContext across concurrent requests, causing data corruption.
Order middleware in the pipeline according to the official ASP.NET Core documentation (ExceptionHandler, HSTS, HttpsRedirection, StaticFiles, Routing, CORS, Authentication, Authorization, custom middleware, endpoints), because middleware executes in registration order and misordering causes authentication to be skipped or CORS headers to be missing.
Use the IOptions<T> / IOptionsSnapshot<T> / IOptionsMonitor<T> pattern for configuration instead of reading IConfiguration directly in services, because the Options pattern provides strong typing, validation via ValidateOnStart(), and hot-reload support for configuration changes without restarting the application.
Configure AddProblemDetails() and UseExceptionHandler() to return RFC 7807 problem details for all error responses instead of returning raw exception messages or custom error shapes, so that API clients can parse errors consistently using a standard format across all endpoints.
Use AddHttpClient<T>() with AddStandardResilienceHandler() from Microsoft.Extensions.Http.Resilience instead of creating HttpClient instances manually, because the factory manages HttpMessageHandler lifetimes (preventing socket exhaustion) and the resilience handler adds retry, circuit breaker, and timeout policies.
Apply rate limiting using AddRateLimiter() with named policies (e.g., "api", "auth") and assign them to endpoint groups via .RequireRateLimiting("api"), rather than implementing custom rate-limiting middleware, so that limits are configurable per-route and testable via the built-in rate-limiting infrastructure.
Use MapGroup() to organize related endpoints under a shared prefix, tag, and filter set instead of repeating .WithTags(), .RequireAuthorization(), and route prefixes on every endpoint, reducing duplication and ensuring that new endpoints in the group inherit the correct policies.
Register health checks using AddHealthChecks().AddDbContextCheck<T>() and .AddCheck<CustomCheck>() and expose them at /health with MapHealthChecks, so that orchestrators (Kubernetes, Azure App Service) can probe application readiness and liveness without hitting business endpoints.
Set builder.Configuration.GetConnectionString() values from environment variables or Azure Key Vault in production rather than hardcoding them in appsettings.json, and use builder.Configuration.AddUserSecrets<Program>() for local development, ensuring secrets never appear in source control.
1---2name: aspnet-core3description: |4 USE FOR: Building web APIs, web applications, and microservices with ASP.NET Core. Use for minimal APIs, controller-based APIs, Razor Pages, MVC, gRPC, and real-time applications with the full ASP.NET Core middleware pipeline, dependency injection, and configuration system.5 DO NOT USE FOR: Desktop or mobile UI (use MAUI, WPF, or Avalonia), game development (use Unity or MonoGame), or standalone console tools that do not serve HTTP traffic.6license: MIT7metadata:8 displayName: ASP.NET Core9 author: "Tyler-R-Kendrick"10 version: "1.0.0"11compatibility:12 - claude13 - copilot14 - cursor15references:16 - title: "ASP.NET Core Documentation"17 url: "https://learn.microsoft.com/en-us/aspnet/core/"18 - title: "ASP.NET Core GitHub Repository"19 url: "https://github.com/dotnet/aspnetcore"20---2122# ASP.NET Core2324## Overview2526ASP.NET Core is Microsoft's cross-platform, high-performance web framework for building modern web APIs, web applications, and microservices. It runs on .NET 6+ and supports minimal APIs (lightweight endpoints), controller-based APIs, Razor Pages, MVC, Blazor, gRPC, and SignalR. ASP.NET Core uses a middleware pipeline for request processing, built-in dependency injection, configuration from multiple sources (JSON, environment variables, Azure Key Vault), and OpenAPI/Swagger integration for API documentation.2728## Minimal API Setup2930Build lightweight HTTP APIs with minimal ceremony using the minimal API pattern.3132```csharp33using Microsoft.AspNetCore.Http.HttpResults;3435var builder = WebApplication.CreateBuilder(args);3637builder.Services.AddEndpointsApiExplorer();38builder.Services.AddSwaggerGen();39builder.Services.AddScoped<IProductRepository, ProductRepository>();40builder.Services.AddProblemDetails();4142var app = builder.Build();4344if (app.Environment.IsDevelopment())45{46 app.UseSwagger();47 app.UseSwaggerUI();48}4950app.UseHttpsRedirection();51app.UseExceptionHandler();5253var products = app.MapGroup("/api/products")54 .WithTags("Products")55 .WithOpenApi();5657products.MapGet("/", async (IProductRepository repo) =>58 TypedResults.Ok(await repo.GetAllAsync()));5960products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (61 int id, IProductRepository repo) =>62{63 var product = await repo.GetByIdAsync(id);64 return product is not null65 ? TypedResults.Ok(product)66 : TypedResults.NotFound();67});6869products.MapPost("/", async Task<Created<Product>> (70 CreateProductRequest request,71 IProductRepository repo) =>72{73 var product = new Product74 {75 Name = request.Name,76 Price = request.Price,77 Category = request.Category78 };79 await repo.AddAsync(product);80 return TypedResults.Created($"/api/products/{product.Id}", product);81}).WithValidationFilter();8283products.MapDelete("/{id:int}", async Task<Results<NoContent, NotFound>> (84 int id, IProductRepository repo) =>85{86 var deleted = await repo.DeleteAsync(id);87 return deleted ? TypedResults.NoContent() : TypedResults.NotFound();88});8990app.Run();9192public record CreateProductRequest(string Name, decimal Price, string Category);93```9495## Middleware Pipeline9697Configure cross-cutting concerns using the middleware pipeline.9899```csharp100using System.Diagnostics;101102var builder = WebApplication.CreateBuilder(args);103104builder.Services.AddAuthentication().AddJwtBearer();105builder.Services.AddAuthorization();106builder.Services.AddRateLimiter(options =>107{108 options.AddFixedWindowLimiter("api", config =>109 {110 config.PermitLimit = 100;111 config.Window = TimeSpan.FromMinutes(1);112 config.QueueLimit = 10;113 });114});115builder.Services.AddResponseCompression();116builder.Services.AddOutputCache();117builder.Services.AddHealthChecks()118 .AddDbContextCheck<AppDbContext>();119120var app = builder.Build();121122// Middleware order matters123app.UseExceptionHandler("/error");124app.UseResponseCompression();125app.UseHttpsRedirection();126app.UseRateLimiter();127app.UseAuthentication();128app.UseAuthorization();129app.UseOutputCache();130131app.MapHealthChecks("/health");132133// Custom middleware134app.Use(async (context, next) =>135{136 var stopwatch = Stopwatch.StartNew();137 context.Response.OnStarting(() =>138 {139 context.Response.Headers["X-Response-Time"] = $"{stopwatch.ElapsedMilliseconds}ms";140 return Task.CompletedTask;141 });142 await next(context);143});144145app.Run();146```147148## Dependency Injection and Service Lifetime149150Register services with the correct lifetime to avoid captive dependency and threading issues.151152```csharp153var builder = WebApplication.CreateBuilder(args);154155// Singleton: shared across all requests (thread-safe required)156builder.Services.AddSingleton<ICacheService, RedisCacheService>();157158// Scoped: one instance per HTTP request159builder.Services.AddScoped<IOrderService, OrderService>();160builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();161builder.Services.AddDbContext<AppDbContext>(options =>162 options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));163164// Transient: new instance every time165builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();166167// Named HttpClient with resilience168builder.Services.AddHttpClient<IExternalApiClient, ExternalApiClient>(client =>169{170 client.BaseAddress = new Uri("https://api.external.com/");171 client.DefaultRequestHeaders.Add("Accept", "application/json");172})173.AddStandardResilienceHandler();174175// Options pattern176builder.Services.Configure<SmtpOptions>(177 builder.Configuration.GetSection("Smtp"));178179var app = builder.Build();180```181182## Error Handling and Problem Details183184Return consistent error responses using the RFC 7807 Problem Details standard.185186```csharp187using Microsoft.AspNetCore.Diagnostics;188using Microsoft.AspNetCore.Mvc;189190var builder = WebApplication.CreateBuilder(args);191builder.Services.AddProblemDetails(options =>192{193 options.CustomizeProblemDetails = context =>194 {195 context.ProblemDetails.Extensions["traceId"] =196 Activity.Current?.Id ?? context.HttpContext.TraceIdentifier;197 };198});199200var app = builder.Build();201202app.UseExceptionHandler(exceptionApp =>203{204 exceptionApp.Run(async context =>205 {206 var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;207 var problemDetails = exception switch208 {209 NotFoundException e => new ProblemDetails210 {211 Status = 404,212 Title = "Resource not found",213 Detail = e.Message214 },215 ValidationException e => new ProblemDetails216 {217 Status = 400,218 Title = "Validation failed",219 Detail = e.Message220 },221 _ => new ProblemDetails222 {223 Status = 500,224 Title = "An unexpected error occurred"225 }226 };227228 context.Response.StatusCode = problemDetails.Status ?? 500;229 await context.Response.WriteAsJsonAsync(problemDetails);230 });231});232```233234## ASP.NET Core Application Models235236| Feature | Minimal APIs | Controllers | Razor Pages | Blazor |237|---|---|---|---|---|238| Use case | Microservices, simple APIs | Full-featured APIs | Server-rendered pages | Interactive web UI |239| Routing | Lambda-based | Attribute/conventional | Page-based | Component-based |240| Model binding | Parameter injection | `[FromBody]`, `[FromQuery]` | `[BindProperty]` | `@bind` |241| Filters | Endpoint filters | Action filters | Page filters | N/A |242| OpenAPI | Built-in | Swashbuckle/NSwag | N/A | N/A |243| Overhead | Minimal | Moderate | Moderate | Higher |244245## Best Practices2462471. **Use `TypedResults` return types (e.g., `Task<Results<Ok<T>, NotFound>>`) on minimal API endpoints** instead of returning `IResult` so that the OpenAPI generator infers response schemas and status codes automatically, producing accurate Swagger documentation without manual `[ProducesResponseType]` attributes.2482492. **Register `DbContext` as `Scoped` (the default for `AddDbContext`) and never inject it into `Singleton` services** because `DbContext` is not thread-safe; injecting a scoped service into a singleton creates a captive dependency that shares a single `DbContext` across concurrent requests, causing data corruption.2502513. **Order middleware in the pipeline according to the official ASP.NET Core documentation** (ExceptionHandler, HSTS, HttpsRedirection, StaticFiles, Routing, CORS, Authentication, Authorization, custom middleware, endpoints), because middleware executes in registration order and misordering causes authentication to be skipped or CORS headers to be missing.2522534. **Use the `IOptions<T>` / `IOptionsSnapshot<T>` / `IOptionsMonitor<T>` pattern for configuration** instead of reading `IConfiguration` directly in services, because the Options pattern provides strong typing, validation via `ValidateOnStart()`, and hot-reload support for configuration changes without restarting the application.2542555. **Configure `AddProblemDetails()` and `UseExceptionHandler()` to return RFC 7807 problem details for all error responses** instead of returning raw exception messages or custom error shapes, so that API clients can parse errors consistently using a standard format across all endpoints.2562576. **Use `AddHttpClient<T>()` with `AddStandardResilienceHandler()` from `Microsoft.Extensions.Http.Resilience`** instead of creating `HttpClient` instances manually, because the factory manages `HttpMessageHandler` lifetimes (preventing socket exhaustion) and the resilience handler adds retry, circuit breaker, and timeout policies.2582597. **Apply rate limiting using `AddRateLimiter()` with named policies** (e.g., `"api"`, `"auth"`) and assign them to endpoint groups via `.RequireRateLimiting("api")`, rather than implementing custom rate-limiting middleware, so that limits are configurable per-route and testable via the built-in rate-limiting infrastructure.2602618. **Use `MapGroup()` to organize related endpoints under a shared prefix, tag, and filter set** instead of repeating `.WithTags()`, `.RequireAuthorization()`, and route prefixes on every endpoint, reducing duplication and ensuring that new endpoints in the group inherit the correct policies.2622639. **Register health checks using `AddHealthChecks().AddDbContextCheck<T>()` and `.AddCheck<CustomCheck>()`** and expose them at `/health` with `MapHealthChecks`, so that orchestrators (Kubernetes, Azure App Service) can probe application readiness and liveness without hitting business endpoints.26426510. **Set `builder.Configuration.GetConnectionString()` values from environment variables or Azure Key Vault in production** rather than hardcoding them in `appsettings.json`, and use `builder.Configuration.AddUserSecrets<Program>()` for local development, ensuring secrets never appear in source control.