ASP.NET Core: Building High-Performance Web Applications and APIs
A practical guide to ASP.NET Core — the cross-platform framework for building REST APIs, MVC applications, and backend services on .NET, covering architecture, minimal APIs, middleware, performance, and modern patterns.
Table of Contents
- Introduction
- Architecture Overview
- Minimal APIs
- MVC and Controllers
- Middleware Pipeline
- Dependency Injection
- Configuration and Options
- Authentication and Authorization
- Performance Features
- Testing and Observability
- Quick Reference Table
- Conclusion
Introduction
ASP.NET Core is a free, open-source, cross-platform framework for building web apps, APIs, and backend services. It’s a ground-up rewrite of the original ASP.NET, designed around three priorities:
- Performance — it’s consistently one of the fastest mainstream web frameworks in independent benchmarks (e.g., TechEmpower).
- Modularity — you opt into only the middleware and services your app actually needs, instead of a fixed, heavyweight pipeline.
- Cross-platform — runs identically on Windows, Linux, and macOS, and deploys to containers, serverless, or bare metal.
This guide covers the core building blocks you’ll use in almost any ASP.NET Core project, from a five-line minimal API to a full MVC application with authentication and background services.
1. Architecture Overview
Every ASP.NET Core app starts from a unified entry point — Program.cs — using the minimal hosting model introduced in .NET 6.
var builder = WebApplication.CreateBuilder(args);
// Register services (dependency injection container)
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline (middleware)
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Enter fullscreen mode Exit fullscreen mode
Two phases matter here:
-
Service registration (
builder.Services...) — declares what your app needs (DI container setup). -
Pipeline configuration (
app.Use.../app.Map...) — declares what happens to each incoming request.
This single-file, top-level-statements style replaced the old Startup.cs + Program.cs split, cutting boilerplate significantly while keeping the same underlying concepts.
2. Minimal APIs
Introduced in .NET 6, minimal APIs let you build HTTP APIs without controllers, attributes, or a large amount of scaffolding — ideal for microservices, small services, and prototypes.
A basic minimal API
var app = builder.Build();
app.MapGet("/products/{id:int}", (int id, IProductRepository repo) =>
{
var product = repo.GetById(id);
return product is not null ? Results.Ok(product) : Results.NotFound();
});
app.MapPost("/products", (Product product, IProductRepository repo) =>
{
repo.Add(product);
return Results.Created($"/products/{product.Id}", product);
});
app.Run();
Enter fullscreen mode Exit fullscreen mode
Route parameters, request bodies, and services are all bound automatically from the method signature — no [FromRoute]/[FromBody] attributes required in the common case (though they’re available when binding is ambiguous).
Route groups (.NET 7+)
Group related endpoints, apply shared configuration (auth, versioning, tags) once:
var products = app.MapGroup("/products").WithTags("Products");
products.MapGet("/", (IProductRepository repo) => repo.GetAll());
products.MapGet("/{id:int}", (int id, IProductRepository repo) => repo.GetById(id));
products.MapPost("/", (Product p, IProductRepository repo) => repo.Add(p))
.RequireAuthorization();
Enter fullscreen mode Exit fullscreen mode
Typed results (.NET 7+)
Results<T1, T2> gives you strongly-typed, OpenAPI-friendly return types instead of the untyped IResult:
app.MapGet("/products/{id}", Results<Ok<Product>, NotFound> (int id, IProductRepository repo) =>
{
var product = repo.GetById(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
});
Enter fullscreen mode Exit fullscreen mode
When to choose minimal APIs vs. controllers
Minimal APIs MVC Controllers Best for Microservices, small APIs, high-throughput endpoints Larger apps, complex validation, filters, conventions Boilerplate Very low Moderate (attributes, base class) Startup performance Faster (less reflection) Slightly slower Structure at scale Requires discipline (route groups, extension methods) Built-in via controllers/areas3. MVC and Controllers
For larger applications, the Model-View-Controller pattern still provides useful structure — action filters, model binding conventions, and view rendering.
A typical API controller
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService) => _orderService = orderService;
[HttpGet("{id:int}")]
public async Task<ActionResult<OrderDto>> GetById(int id)
{
var order = await _orderService.GetByIdAsync(id);
return order is not null ? Ok(order) : NotFound();
}
[HttpPost]
public async Task<ActionResult<OrderDto>> Create(CreateOrderRequest request)
{
var order = await _orderService.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
}
}
Enter fullscreen mode Exit fullscreen mode
The [ApiController] attribute enables automatic model-state validation (400 responses on invalid input), binding source inference, and problem-details error responses — all without extra code.
Razor Pages and MVC Views
For server-rendered HTML, ASP.NET Core supports both Razor Pages (page-focused, one file per page) and traditional MVC views (controller + shared view folder):
// Razor Page handler (Pages/Products/Index.cshtml.cs)
public class IndexModel : PageModel
{
private readonly IProductRepository _repo;
public IndexModel(IProductRepository repo) => _repo = repo;
public IList<Product> Products { get; set; } = [];
public async Task OnGetAsync() => Products = await _repo.GetAllAsync();
}
Enter fullscreen mode Exit fullscreen mode
@* Pages/Products/Index.cshtml *@
@model IndexModel
<ul>
@foreach (var product in Model.Products)
{
<li>@product.Name — @product.Price.ToString("C")</li>
}
</ul>
Enter fullscreen mode Exit fullscreen mode
Blazor as an alternative UI model
For interactive UIs without hand-written JavaScript, Blazor (Server or WebAssembly) lets you write component-based UI in C# that runs either on the server over SignalR or directly in the browser via WebAssembly. It’s a separate topic in its own right, but it plugs into the same ASP.NET Core hosting model.
4. Middleware Pipeline
Middleware is the heart of how ASP.NET Core processes requests: each piece of middleware can inspect, short-circuit, or pass along the request to the next one, forming a chain.
The pipeline as nested delegates
app.Use(async (context, next) =>
{
var stopwatch = Stopwatch.StartNew();
await next(context); // call the next middleware in the chain
stopwatch.Stop();
Console.WriteLine($"{context.Request.Path} took {stopwatch.ElapsedMilliseconds}ms");
});
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
Enter fullscreen mode Exit fullscreen mode
Order matters. Authentication must run before authorization; exception handling should wrap almost everything; static files are usually served before routing kicks in for dynamic endpoints.
Writing custom middleware as a class
public class RequestLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestLoggingMiddleware> _logger;
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
_logger.LogInformation("Handling {Method} {Path}", context.Request.Method, context.Request.Path);
await _next(context);
_logger.LogInformation("Response: {StatusCode}", context.Response.StatusCode);
}
}
// registration
app.UseMiddleware<RequestLoggingMiddleware>();
Enter fullscreen mode Exit fullscreen mode
Endpoint routing
Since ASP.NET Core 3.0, routing is decoupled from MVC — UseRouting() and UseEndpoints() (or the simplified Map* calls) let middleware run with full knowledge of which endpoint will ultimately handle the request, enabling things like endpoint-specific authorization policies.
5. Dependency Injection
ASP.NET Core has a built-in DI container — no third-party library required for the common cases.
Service lifetimes
builder.Services.AddSingleton<ICacheService, MemoryCacheService>(); // one instance for the app's lifetime
builder.Services.AddScoped<IOrderService, OrderService>(); // one instance per HTTP request
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // new instance every time it's requested
Enter fullscreen mode Exit fullscreen mode
Lifetime Instance created Typical useSingleton
Once, shared everywhere
Caches, configuration, thread-safe stateless services
Scoped
Once per request
DbContext, per-request state
Transient
Every injection
Lightweight, stateless helpers
Constructor injection
public class OrderService : IOrderService
{
private readonly AppDbContext _db;
private readonly IEmailSender _emailSender;
public OrderService(AppDbContext db, IEmailSender emailSender)
{
_db = db;
_emailSender = emailSender;
}
public async Task<Order> CreateAsync(CreateOrderRequest request)
{
var order = new Order { /* ... */ };
_db.Orders.Add(order);
await _db.SaveChangesAsync();
await _emailSender.SendOrderConfirmationAsync(order);
return order;
}
}
Enter fullscreen mode Exit fullscreen mode
Keyed services (.NET 8+)
Register multiple implementations of the same interface and resolve by key:
builder.Services.AddKeyedSingleton<INotifier, EmailNotifier>("email");
builder.Services.AddKeyedSingleton<INotifier, SmsNotifier>("sms");
public class AlertService([FromKeyedServices("sms")] INotifier notifier) { /* ... */ }
Enter fullscreen mode Exit fullscreen mode
6. Configuration and Options
Configuration in ASP.NET Core is layered: appsettings.json, environment-specific overrides, environment variables, and command-line args all merge together automatically.
appsettings.json
{
"ConnectionStrings": {
"Default": "Server=...;Database=...;"
},
"EmailSettings": {
"SmtpHost": "smtp.example.com",
"Port": 587
}
}
Enter fullscreen mode Exit fullscreen mode
Strongly-typed options with validation
public class EmailSettings
{
public required string SmtpHost { get; set; }
public int Port { get; set; }
}
builder.Services
.AddOptions<EmailSettings>()
.Bind(builder.Configuration.GetSection("EmailSettings"))
.ValidateDataAnnotations()
.ValidateOnStart();
Enter fullscreen mode Exit fullscreen mode
public class EmailSender(IOptions<EmailSettings> options)
{
private readonly EmailSettings _settings = options.Value;
}
Enter fullscreen mode Exit fullscreen mode
ValidateOnStart() (added in .NET 8) fails fast at application startup if configuration is invalid, instead of surfacing the error deep inside a request months later.
7. Authentication and Authorization
JWT Bearer authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://your-identity-provider.com";
options.Audience = "your-api";
});
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
Enter fullscreen mode Exit fullscreen mode
Policy-based authorization
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdmin", policy => policy.RequireRole("Admin"));
options.AddPolicy("MinimumAge", policy =>
policy.RequireAssertion(context =>
context.User.HasClaim(c => c.Type == "Age" && int.Parse(c.Value) >= 18)));
});
Enter fullscreen mode Exit fullscreen mode
[Authorize(Policy = "RequireAdmin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id) => NoContent();
Enter fullscreen mode Exit fullscreen mode
Identity and ASP.NET Core Identity
For apps needing full user management (registration, password reset, external logins), ASP.NET Core Identity provides a ready-made user store, and .NET 8 added built-in Identity API endpoints (MapIdentityApi<TUser>()) for token-based auth without hand-rolling the endpoints yourself.
8. Performance Features
ASP.NET Core’s performance reputation comes from a combination of the runtime (Kestrel, the built-in web server) and framework-level design choices.
Kestrel: the built-in web server
Kestrel is a cross-platform, event-driven web server built directly on top of libuv/managed sockets. It’s fast enough to be used directly in production (often behind a reverse proxy like Nginx, YARP, or a cloud load balancer for TLS termination and multiplexing).
Response caching and output caching
builder.Services.AddOutputCache(options =>
{
options.AddPolicy("Expire60", b => b.Expire(TimeSpan.FromSeconds(60)));
});
app.UseOutputCache();
app.MapGet("/weather", () => GetWeatherForecast())
.CacheOutput("Expire60");
Enter fullscreen mode Exit fullscreen mode
Output caching (added in .NET 7) caches the full response server-side, avoiding recomputation for repeated identical requests — a major win for read-heavy endpoints.
Response compression
builder.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
});
Enter fullscreen mode Exit fullscreen mode
Rate limiting (.NET 7+)
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("fixed", opt =>
{
opt.Window = TimeSpan.FromSeconds(10);
opt.PermitLimit = 20;
});
});
app.UseRateLimiter();
app.MapGet("/data", () => "ok").RequireRateLimiting("fixed");
Enter fullscreen mode Exit fullscreen mode
Built-in rate limiting removes the need for a third-party package for common scenarios (fixed window, sliding window, token bucket, concurrency limiting).
Native AOT (.NET 8+)
Ahead-of-time compilation produces a self-contained native executable with no JIT step, drastically reducing cold-start time and memory footprint — ideal for containers and serverless functions:
dotnet publish -r linux-x64 -c Release /p:PublishAot=true
Enter fullscreen mode Exit fullscreen mode
Native AOT currently works best with minimal APIs and trimmable code (some reflection-heavy libraries need adaptation).
HTTP/2, HTTP/3, and gRPC
Kestrel supports HTTP/2 and HTTP/3 (QUIC) out of the box, and ASP.NET Core has first-class support for gRPC for high-performance, strongly-typed service-to-service communication — a common backend-for-backend alternative to REST.
9. Testing and Observability
Integration testing with WebApplicationFactory
public class ProductsApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly HttpClient _client;
public ProductsApiTests(WebApplicationFactory<Program> factory) =>
_client = factory.CreateClient();
[Fact]
public async Task GetProduct_ReturnsOk()
{
var response = await _client.GetAsync("/products/1");
response.EnsureSuccessStatusCode();
}
}
Enter fullscreen mode Exit fullscreen mode
WebApplicationFactory<TEntryPoint> spins up an in-memory test server using your real Program.cs configuration, letting you test the full middleware pipeline without a real network hop.
Health checks
builder.Services.AddHealthChecks()
.AddSqlServer(connectionString)
.AddCheck<CustomHealthCheck>("custom");
app.MapHealthChecks("/health");
Enter fullscreen mode Exit fullscreen mode
OpenTelemetry (built-in support since .NET 8)
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddAspNetCoreInstrumentation().AddHttpClientInstrumentation())
.WithMetrics(metrics => metrics.AddAspNetCoreInstrumentation());
Enter fullscreen mode Exit fullscreen mode
Distributed tracing and metrics are now first-class, framework-level concerns rather than something bolted on via a separate agent.
Quick Reference Table
Feature Introduced Problem it Solves Minimal hosting model .NET 6 Program.cs/Startup.cs split boilerplate Minimal APIs .NET 6 Heavyweight controllers for small services Route groups .NET 7 Repeating config across related endpoints Typed results .NET 7 UntypedIResult return values
Keyed DI services
.NET 8
Multiple implementations of one interface
ValidateOnStart()
.NET 8
Late-discovered configuration errors
Output caching
.NET 7
Recomputing identical responses
Rate limiting middleware
.NET 7
Third-party rate-limit packages
Native AOT
.NET 8
Slow cold starts, high memory footprint
Built-in OpenTelemetry
.NET 8
Bolted-on tracing/metrics agents
Identity API endpoints
.NET 8
Hand-rolled auth endpoints
Conclusion
ASP.NET Core’s evolution has been about removing friction without removing power: minimal APIs and the unified Program.cs cut boilerplate for small services, while controllers, Razor Pages, and Blazor remain available for larger, more structured applications. Underneath it all, the framework keeps investing in things that matter at scale — Kestrel’s raw throughput, built-in caching and rate limiting, native AOT for fast cold starts, and first-class observability — so that the same framework can comfortably run a five-line prototype and a production system handling millions of requests a day.
The common thread is that ASP.NET Core increasingly lets you pay only for the complexity you actually need, while making the on-ramp to production-grade performance, security, and observability a built-in part of the framework rather than a pile of third-party packages.
Found this useful? Feel free to star the repo, open an issue with corrections, or share which ASP.NET Core feature has saved you the most time.
답글 남기기