Program.cs Deep Dive
🎯 What You'll Learn
- Understanding the Program.cs file structure
- The WebApplicationBuilder and its purpose
- Configuring services with dependency injection
- The middleware pipeline and request processing
- Understanding app.Run() and the hosting model
- Common middleware and their order
- Customizing your application startup
- Best practices for Program.cs organization
The Default Program.cs
When you create a new Web API project, you get a Program.cs file that looks
like this:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
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();
app.UseAuthorization();
app.MapControllers();
app.Run();
This file is the entry point of your application. Let's break it down line by line.
Part 1: Building the Application
Creating the Builder
var builder = WebApplication.CreateBuilder(args);
WebApplication.CreateBuilder(args) creates a WebApplicationBuilder
that configures:
- Configuration: Loads appsettings.json, environment variables, command-line args
- Logging: Sets up default logging providers
- Dependency Injection: Initializes the service container
- Hosting: Configures Kestrel web server
The builder pattern lets you configure your application step by step before actually
creating it. Think of it like configuring a car before it's built—you specify the
engine, wheels, color, etc., then call Build() to assemble it.
Registering Services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services is the dependency injection container.
Here we're registering services:
| Method | What It Does |
|---|---|
AddControllers() |
Registers MVC controllers and related services |
AddEndpointsApiExplorer() |
Enables API endpoint discovery for Swagger |
AddSwaggerGen() |
Registers Swagger/OpenAPI documentation generator |
Building the Application
var app = builder.Build();
Build() creates the WebApplication instance. After this point:
- You can't add more services
- You configure the middleware pipeline
- You define how requests are processed
Part 2: Configuring the Middleware Pipeline
What is Middleware?
Middleware are components that form a pipeline to process HTTP requests and responses. Each middleware can:
- Process the incoming request
- Pass the request to the next middleware
- Process the outgoing response
- Short-circuit the pipeline (return a response immediately)
Request → [Middleware 1] → [Middleware 2] → [Middleware 3] → Endpoint
↓ ↓ ↓ ↓
Logging CORS Authentication Controller
↑ ↑ ↑ ↑
Response ← [Middleware 1] ← [Middleware 2] ← [Middleware 3] ← Endpoint
Environment-Specific Middleware
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
This code only runs in Development environment. Swagger UI is useful for testing but shouldn't be exposed in production.
Common Middleware
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
| Middleware | Purpose |
|---|---|
UseHttpsRedirection() |
Redirects HTTP requests to HTTPS |
UseAuthorization() |
Enables authorization checks |
MapControllers() |
Maps controller endpoints to routes |
The order you add middleware is critical. For example:
• UseAuthentication() must come before UseAuthorization()
• UseRouting() must come before UseAuthorization()
• UseCors() should be early in the pipeline
Starting the Application
app.Run();
Run() starts the web server and begins listening for requests. This is a
blocking call—your application runs until you stop it (Ctrl+C).
A More Complete Example
Here's a more realistic Program.cs for InvenTrack:
using Microsoft.EntityFrameworkCore;
using Serilog;
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
try
{
Log.Information("Starting InvenTrack API");
var builder = WebApplication.CreateBuilder(args);
// Add Serilog
builder.Host.UseSerilog();
// Add services to the container
builder.Services.AddControllers();
// Add database context
builder.Services.AddDbContext<InvenTrackDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Add CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("http://localhost:3000")
.AllowAnyMethod()
.AllowAnyHeader();
});
});
// Add authentication
builder.Services.AddAuthentication()
.AddJwtBearer();
// Add Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register application services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// Middleware pipeline (order matters!)
app.UseSerilogRequestLogging();
app.UseHttpsRedirection();
app.UseCors("AllowFrontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
Understanding Service Lifetimes
When registering services, you choose a lifetime:
Transient
builder.Services.AddTransient<IEmailService, EmailService>();
- Created every time they're requested
- Best for lightweight, stateless services
- Example: Email sender, PDF generator
Scoped
builder.Services.AddScoped<IProductService, ProductService>();
- Created once per HTTP request
- Best for services that work with databases
- Example: Repository, DbContext, business services
Singleton
builder.Services.AddSingleton<ICacheService, CacheService>();
- Created once for the application lifetime
- Shared across all requests
- Must be thread-safe!
- Example: Configuration, cache, logging
Default choice: Use Scoped for most services
Stateless/lightweight: Use Transient
Expensive to create: Use Singleton (if thread-safe)
DbContext: Always Scoped
Common Middleware in Order
Here's the recommended middleware order for most applications:
// 1. Exception handling (catch errors early)
app.UseExceptionHandler("/error");
// 2. HSTS (HTTP Strict Transport Security)
app.UseHsts();
// 3. HTTPS redirection
app.UseHttpsRedirection();
// 4. Static files (if serving HTML/CSS/JS)
app.UseStaticFiles();
// 5. Routing
app.UseRouting();
// 6. CORS (must be after UseRouting, before UseAuthentication)
app.UseCors("MyPolicy");
// 7. Authentication (who are you?)
app.UseAuthentication();
// 8. Authorization (what can you do?)
app.UseAuthorization();
// 9. Custom middleware
app.UseMiddleware<RequestTimingMiddleware>();
// 10. Endpoints (controllers, minimal APIs)
app.MapControllers();
Custom Middleware
You can create custom middleware for cross-cutting concerns:
Inline Middleware
app.Use(async (context, next) =>
{
// Before the next middleware
Console.WriteLine($"Request: {context.Request.Path}");
// Call next middleware
await next();
// After the next middleware
Console.WriteLine($"Response: {context.Response.StatusCode}");
});
Class-Based Middleware
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 stopwatch = Stopwatch.StartNew();
await _next(context);
stopwatch.Stop();
_logger.LogInformation(
"Request {Method} {Path} took {ElapsedMilliseconds}ms",
context.Request.Method,
context.Request.Path,
stopwatch.ElapsedMilliseconds
);
}
}
Register it in Program.cs:
app.UseMiddleware<RequestTimingMiddleware>();
Accessing Configuration
The builder provides access to configuration:
// Get connection string
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
// Get app setting
var apiKey = builder.Configuration["ApiKey"];
// Get section
var jwtSettings = builder.Configuration.GetSection("JwtSettings");
var secret = jwtSettings["Secret"];
Environment Detection
if (app.Environment.IsDevelopment())
{
// Development-only code
}
if (app.Environment.IsProduction())
{
// Production-only code
}
if (app.Environment.IsEnvironment("Staging"))
{
// Staging-only code
}
Key Takeaways
Program.csis the entry point of your applicationWebApplicationBuilderconfigures services and settingsbuilder.Servicesis the dependency injection container- Service lifetimes: Transient, Scoped, Singleton
builder.Build()creates theWebApplication- Middleware forms a pipeline to process requests
- Middleware order matters—follow the recommended sequence
app.Run()starts the web server- Use
app.Environmentto detect Development/Production - Custom middleware can be inline or class-based
- Configuration is accessed via
builder.Configuration
You now understand how ASP.NET Core applications start up and how the middleware pipeline works! In the next section, we'll dive into Configuration & appsettings.json— how to manage application settings, connection strings, and environment-specific configuration.