ASP.NET Core & Minimal APIs Foundations
Master the fundamentals of ASP.NET Core architecture, Minimal APIs concepts, and environment setup for building modern web APIs with .NET 10
ASP.NET Core and Minimal APIs Foundations
Master ASP.NET Core and Minimal APIs with free flashcards and spaced repetition to solidify your understanding. This lesson covers the fundamentals of ASP.NET Core architecture, dependency injection, middleware pipeline configuration, and building lightweight HTTP APIs using the Minimal API patternβessential skills for modern .NET 10 web development.
Welcome π»
Welcome to the foundations of ASP.NET Core and Minimal APIs! Whether you're building enterprise web applications or lightweight microservices, understanding these core concepts will empower you to create high-performance, scalable applications with .NET 10.
ASP.NET Core represents a complete redesign of the traditional ASP.NET framework. It's cross-platform, modular, high-performance, and built from the ground up with modern development practices in mind. Minimal APIs, introduced in .NET 6 and enhanced in .NET 10, provide a streamlined approach to building HTTP APIs with minimal ceremony and maximum performance.
π― What You'll Learn:
- ASP.NET Core architecture and request pipeline
- Dependency Injection (DI) fundamentals
- Middleware components and ordering
- Minimal API syntax and routing
- Configuration and environment management
- Testing strategies for Minimal APIs
Core Concepts
ποΈ ASP.NET Core Architecture
ASP.NET Core applications are built around a request pipeline that processes incoming HTTP requests through a series of middleware components. Think of it like an assembly line in a factoryβeach station (middleware) performs a specific task before passing the request to the next station.
βββββββββββββββββββββββββββββββββββββββββββββββ
β ASP.NET CORE REQUEST PIPELINE β
βββββββββββββββββββββββββββββββββββββββββββββββ
HTTP Request
β
βΌ
βββββββββββββββ
β Exception β β Catches errors from downstream
β Handler β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Static β β Serves CSS, JS, images
β Files β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Routing β β Matches URL to endpoint
β β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Auth β β Validates identity
β β
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββ
β Endpoint β β Your application logic
β Execution β
ββββββββ¬βββββββ
β
βΌ
HTTP Response
Key architectural principles:
- Modularity: Only include what you need via NuGet packages
- Dependency Injection: Built-in DI container for loose coupling
- Configuration: Flexible configuration from multiple sources
- Hosting: Can run in IIS, Kestrel, Docker, or as a standalone executable
π Dependency Injection (DI)
Dependency Injection is a design pattern where objects receive their dependencies from an external source rather than creating them internally. ASP.NET Core has DI baked into its core.
π‘ Real-world analogy: Imagine you're a chef (your service). Instead of growing your own vegetables, raising livestock, and milling flour (creating dependencies), ingredients are delivered to your kitchen (injected). You focus on cooking, not farming.
Service Lifetimes:
| Lifetime | Description | Use Case |
|---|---|---|
| Transient | Created each time requested | Lightweight, stateless services |
| Scoped | Created once per request | Database contexts, request-specific data |
| Singleton | Created once for application lifetime | Configuration, caching, logging |
π§ Memory Device - "TSS": Transient (Temporary), Scoped (Single request), Singleton (Stays forever)
π Middleware Pipeline
Middleware components form a pipeline that handles requests and responses. Each middleware can:
- Process the incoming request
- Call the next middleware in the pipeline
- Process the outgoing response
Middleware order matters! Early middleware can short-circuit the pipeline by not calling next().
ββββββββββββββββββββββββββββββββββββββββββ
β MIDDLEWARE EXECUTION FLOW β
ββββββββββββββββββββββββββββββββββββββββββ
Request ββββ
ββββ Middleware 1 βββ
β β
β βββββββββββββββββ
β β
ββββββΌβββ Middleware 2 βββ
β β β
β β βββββββββββββββββ
β β β
ββββββΌβββββΌβββ Endpoint
β β
β ββββ Middleware 2 βββ
β β
ββββββββ Middleware 1 βββββ€
β
Response βββββββββββββββββββββββββββββββββββ
β‘ Minimal APIs
Minimal APIs reduce the boilerplate required for building HTTP APIs. Instead of controllers with classes, attributes, and method signatures, you define endpoints with concise lambda expressions.
Traditional Controller vs Minimal API:
| Traditional Controller | Minimal API |
|---|---|
| β’ Requires controller class β’ Uses attributes for routing β’ More ceremony, more files β’ Better for complex scenarios |
β’ Routes defined inline β’ Lambda-based handlers β’ Less boilerplate β’ Perfect for microservices |
Minimal API Philosophy:
- Start simple, add complexity only when needed
- Optimize for developer productivity
- Excellent performance characteristics
- Easier to understand and maintain for simple scenarios
π€ Did you know? Minimal APIs can achieve up to 30% better throughput than controller-based APIs in benchmarks due to reduced allocations and simpler execution paths!
πΊοΈ Routing in Minimal APIs
Routing maps incoming HTTP requests to specific handlers based on:
- HTTP Method (GET, POST, PUT, DELETE, etc.)
- URL Pattern (e.g.,
/products/{id}) - Route Constraints (e.g.,
{id:int}for integer-only)
Route Parameters:
{id}- Captures any value{id:int}- Only matches integers{name:alpha}- Only matches alphabetic characters{category?}- Optional parameter
βοΈ Configuration System
ASP.NET Core uses a flexible configuration system that can read from multiple sources:
- appsettings.json (base configuration)
- appsettings..json (environment-specific)
- Environment variables (deployment-specific)
- Command-line arguments (runtime overrides)
- User secrets (development-only sensitive data)
Configuration hierarchy (later sources override earlier ones):
appsettings.json
β
appsettings.Development.json
β
Environment Variables
β
Command-line Arguments
β
FINAL CONFIG
Examples
Example 1: Basic Minimal API Setup π
Let's create a simple "Hello World" Minimal API that demonstrates the core structure:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Define endpoints
app.MapGet("/", () => "Hello World!");
app.MapGet("/hello/{name}", (string name) =>
$"Hello, {name}!");
app.Run();
Explanation:
WebApplication.CreateBuilder(args)creates the builder with default configurationbuilder.Servicesis where you register dependencies for DIapp.Use...()methods add middleware to the pipelineapp.MapGet()defines GET endpoints with inline handlers- Route parameters (like
{name}) are automatically bound to method parameters
π‘ Try this: Run the application and navigate to https://localhost:5001/hello/YourName to see parameter binding in action!
Example 2: Dependency Injection with Services π§
Here's how to create and inject a custom service:
public interface IProductService
{
Task<List<Product>> GetAllProductsAsync();
Task<Product?> GetProductByIdAsync(int id);
}
public class ProductService : IProductService
{
private readonly List<Product> _products = new()
{
new Product { Id = 1, Name = "Laptop", Price = 999.99m },
new Product { Id = 2, Name = "Mouse", Price = 29.99m },
new Product { Id = 3, Name = "Keyboard", Price = 79.99m }
};
public Task<List<Product>> GetAllProductsAsync()
{
return Task.FromResult(_products);
}
public Task<Product?> GetProductByIdAsync(int id)
{
return Task.FromResult(_products.FirstOrDefault(p => p.Id == id));
}
}
public record Product(int Id, string Name, decimal Price);
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Register the service with Scoped lifetime
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
// Inject the service into endpoints
app.MapGet("/products", async (IProductService productService) =>
{
var products = await productService.GetAllProductsAsync();
return Results.Ok(products);
});
app.MapGet("/products/{id:int}", async (int id, IProductService productService) =>
{
var product = await productService.GetProductByIdAsync(id);
return product is not null
? Results.Ok(product)
: Results.NotFound();
});
app.Run();
Explanation:
AddScoped<TInterface, TImplementation>()registers the service with scoped lifetime- Parameters in endpoint handlers are automatically resolved from DI
Results.Ok()andResults.NotFound()return typed responses with proper status codes{id:int}constraint ensures only integer IDs are matched
Example 3: Custom Middleware Component π
Middleware can intercept requests and responses for cross-cutting concerns:
public class RequestTimingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestTimingMiddleware> _logger;
public RequestTimingMiddleware(
RequestDelegate next,
ILogger<RequestTimingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
var startTime = DateTime.UtcNow;
// Call the next middleware in the pipeline
await _next(context);
var duration = DateTime.UtcNow - startTime;
_logger.LogInformation(
"Request {Method} {Path} completed in {Duration}ms",
context.Request.Method,
context.Request.Path,
duration.TotalMilliseconds);
}
}
// Extension method for cleaner registration
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestTiming(
this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestTimingMiddleware>();
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// Add custom middleware early in pipeline
app.UseRequestTiming();
app.MapGet("/slow", async () =>
{
await Task.Delay(500); // Simulate slow operation
return "Done!";
});
app.Run();
Explanation:
- Middleware receives
RequestDelegate _nextto call the next component InvokeAsyncis called for each request- Timing logic wraps the call to
_next(context) - Extension methods provide clean syntax:
app.UseRequestTiming()
β οΈ Important: Register middleware in the correct order! The timing middleware should come early so it measures the entire pipeline.
Example 4: POST Endpoints with Validation π
Handling POST requests with model binding and validation:
using System.ComponentModel.DataAnnotations;
public record CreateProductRequest
{
[Required]
[StringLength(100, MinimumLength = 3)]
public string Name { get; init; } = string.Empty;
[Range(0.01, 10000.00)]
public decimal Price { get; init; }
[StringLength(500)]
public string? Description { get; init; }
}
public record ProductResponse(int Id, string Name, decimal Price);
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Enable model validation
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
var products = new List<ProductResponse>();
var nextId = 1;
app.MapPost("/products", (CreateProductRequest request) =>
{
// Model binding and validation happen automatically
var product = new ProductResponse(
nextId++,
request.Name,
request.Price);
products.Add(product);
return Results.Created($"/products/{product.Id}", product);
});
app.MapPut("/products/{id:int}", (int id, CreateProductRequest request) =>
{
var existingProduct = products.FirstOrDefault(p => p.Id == id);
if (existingProduct is null)
return Results.NotFound();
var updatedProduct = new ProductResponse(id, request.Name, request.Price);
products.Remove(existingProduct);
products.Add(updatedProduct);
return Results.Ok(updatedProduct);
});
app.MapDelete("/products/{id:int}", (int id) =>
{
var product = products.FirstOrDefault(p => p.Id == id);
if (product is null)
return Results.NotFound();
products.Remove(product);
return Results.NoContent();
});
app.Run();
Explanation:
- Data annotations (
[Required],[Range]) provide validation rules recordtypes are perfect for DTOsβimmutable and conciseResults.Created()returns 201 status with Location headerResults.NoContent()returns 204 for successful DELETE operations- Model binding automatically deserializes JSON request bodies
π‘ Pro tip: Use separate request/response models to avoid over-posting vulnerabilities and maintain clean API contracts.
β οΈ Common Mistakes
1. Incorrect Middleware Order
β Wrong:
app.UseRouting();
app.UseStaticFiles(); // Too late!
app.UseAuthentication();
β Correct:
app.UseStaticFiles(); // Early for performance
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
Why it matters: Static files shouldn't go through authentication. Put UseStaticFiles() early to short-circuit unnecessary processing.
2. Wrong Service Lifetime
β Wrong:
// DbContext as Singleton - leads to threading issues!
builder.Services.AddSingleton<MyDbContext>();
β Correct:
// DbContext should be Scoped (per request)
builder.Services.AddDbContext<MyDbContext>();
Why it matters: DbContext isn't thread-safe. Using Singleton lifetime causes race conditions and data corruption.
3. Forgetting async/await
β Wrong:
app.MapGet("/data", (IDataService service) =>
{
var data = service.GetDataAsync(); // Returns Task, not data!
return Results.Ok(data);
});
β Correct:
app.MapGet("/data", async (IDataService service) =>
{
var data = await service.GetDataAsync();
return Results.Ok(data);
});
4. Injecting Scoped Services into Singletons
β Wrong:
public class MySingletonService
{
private readonly MyDbContext _db; // DANGER!
public MySingletonService(MyDbContext db)
{
_db = db; // Captures a scoped service!
}
}
β Correct:
public class MySingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public MySingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task DoWork()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyDbContext>();
// Use db here
}
}
5. Not Using Route Constraints
β Wrong:
app.MapGet("/products/{id}", (string id) =>
{
if (!int.TryParse(id, out var numericId))
return Results.BadRequest();
// ...
});
β Correct:
app.MapGet("/products/{id:int}", (int id) =>
{
// id is guaranteed to be an integer
// ...
});
Why it matters: Route constraints prevent invalid requests from reaching your handler and provide automatic 404 responses for mismatches.
Key Takeaways π―
β ASP.NET Core is a cross-platform, high-performance framework with built-in DI, modular architecture, and flexible configuration
β Minimal APIs reduce boilerplate while maintaining performanceβperfect for microservices and simple APIs
β Middleware order mattersβearly middleware can short-circuit the pipeline for performance
β Service lifetimes (Transient, Scoped, Singleton) control how dependencies are created and shared
β
Route constraints ({id:int}) validate parameters before your handler runs
β Model binding automatically deserializes request bodies and validates using data annotations
β
Results helpers (Results.Ok(), Results.NotFound(), etc.) provide type-safe HTTP responses
β Always use async/await for I/O operationsβnever block the thread pool
π Further Study
- Official ASP.NET Core Documentation - https://docs.microsoft.com/aspnet/core/
- Minimal APIs Overview - https://docs.microsoft.com/aspnet/core/fundamentals/minimal-apis
- Dependency Injection in .NET - https://docs.microsoft.com/dotnet/core/extensions/dependency-injection
π Quick Reference Card
| WebApplication.CreateBuilder() | Initializes app with default config |
| builder.Services.Add... | Registers services for DI |
| app.Use...() | Adds middleware to pipeline |
| app.MapGet/Post/Put/Delete() | Defines HTTP endpoints |
| Transient | New instance every time |
| Scoped | One instance per request |
| Singleton | One instance for app lifetime |
| {param:type} | Route constraint (int, alpha, guid, etc.) |
| Results.Ok/NotFound/Created() | Type-safe HTTP result helpers |
| ILogger<T> | Built-in logging abstraction |
π§ Remember: Start simple with Minimal APIs, add complexity (controllers, filters, etc.) only when your scenario demands it. The best code is code you don't have to write!