Startup and Program.cs — The Modern Minimal API Bootstrap

ASP.NET Core’s startup has two distinct phases: the build phase (register services, configure options, add middleware) and the run phase (map routes, process requests). In .NET 6+, both phases live in a single Program.cs file using the minimal API hosting model — no separate Startup class required. A well-organised Program.cs uses extension methods to group related registrations and is easy to read and modify. Understanding the two phases and the order of middleware registration is the foundation of every ASP.NET Core application you will build in this series.

The Two Phases of ASP.NET Core Startup

// ── PHASE 1: BUILD — register services (before builder.Build()) ───────────
var builder = WebApplication.CreateBuilder(args);

// Service registrations
builder.Services.AddControllers()
    .AddJsonOptions(opts =>
    {
        opts.JsonSerializerOptions.PropertyNamingPolicy   = JsonNamingPolicy.CamelCase;
        opts.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    });

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Application services
builder.Services.AddScoped<IPostRepository, EfPostRepository>();
builder.Services.AddScoped<IPostService,    PostService>();
builder.Services.AddScoped<IEmailSender,    SmtpEmailSender>();

// Infrastructure
builder.Services.AddDbContext<AppDbContext>(opts =>
    opts.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

// ── Build the host — DI container is now sealed ───────────────────────────
var app = builder.Build();

// ── PHASE 2: RUN — configure middleware pipeline (after Build()) ──────────
// Order matters — middleware runs in registration order

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler();   // production error handler
}

app.UseHttpsRedirection();
app.UseRouting();           // must be before UseAuthentication and UseAuthorization
app.UseAuthentication();    // must be before UseAuthorization
app.UseAuthorization();

app.MapControllers();       // maps all [Route] and [HttpGet/Post/...] controller actions

app.Run();
Note: Middleware registration order is critical. UseRouting() must come before UseAuthentication() and UseAuthorization() — routing must identify the endpoint before auth middleware can check its permissions. UseAuthentication() must come before UseAuthorization() — you must establish who the user is before checking what they can do. Exception handling middleware (UseExceptionHandler()) should be first in the pipeline so it can catch exceptions from all subsequent middleware. Static files should come before routing to avoid unnecessary authentication checks on static assets.
Tip: Extract large Program.cs into extension methods to keep it readable. Create a ServiceCollectionExtensions static class with methods like AddApplicationServices(this IServiceCollection services), AddInfrastructure(this IServiceCollection services, IConfiguration config), and AddApiServices(this IServiceCollection services). Program.cs then reads like a table of contents: builder.Services.AddApplicationServices().AddInfrastructure(builder.Configuration).AddApiServices(). This pattern is used in the ASP.NET Core framework itself for AddControllers(), AddAuthentication(), etc.
Warning: Do not resolve services from the root IServiceProvider (app.Services) to perform application-level initialisation work. The root provider creates singleton-lifetime objects but does not support scoped services (it throws InvalidOperationException: Cannot resolve scoped service 'IPostRepository' from root provider). If you need to seed the database or warm caches on startup, do it inside a manually created scope: using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); await db.Database.MigrateAsync();

Organising Program.cs with Extension Methods

// Program.cs — clean, readable with extension methods
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddApiServices()
    .AddApplicationServices()
    .AddInfrastructure(builder.Configuration);

var app = builder.Build();

app.UseApiMiddleware(app.Environment);
app.MapControllers();
app.Run();

// ── ServiceCollectionExtensions.cs ────────────────────────────────────────
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddApiServices(this IServiceCollection services)
    {
        services.AddControllers().AddJsonOptions(/* ... */);
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen(/* ... */);
        services.AddExceptionHandler<GlobalExceptionHandler>();
        services.AddProblemDetails();
        return services;
    }

    public static IServiceCollection AddApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IPostService, PostService>();
        services.AddScoped<IUserService, UserService>();
        return services;
    }

    public static IServiceCollection AddInfrastructure(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        services.AddDbContext<AppDbContext>(opts =>
            opts.UseSqlServer(configuration.GetConnectionString("Default")));
        services.AddScoped<IPostRepository, EfPostRepository>();
        services.AddSingleton<IEmailSender, SmtpEmailSender>();
        return services;
    }
}

Common Mistakes

Mistake 1 — Wrong middleware order (auth before routing)

❌ Wrong — UseAuthorization before UseRouting; endpoint metadata not yet available:

app.UseAuthorization();   // cannot check endpoint policies — routing not done yet!
app.UseRouting();

✅ Correct order: UseRouting → UseAuthentication → UseAuthorization → MapControllers.

Mistake 2 — Resolving scoped services from the root provider at startup

❌ Wrong — throws InvalidOperationException for scoped services.

✅ Correct — create a scope explicitly: using var scope = app.Services.CreateScope();

🧠 Test Yourself

A developer registers UseAuthentication() after MapControllers(). What is the consequence?