When building ASP.NET Core applications, the Program.cs
file can quickly become overwhelming as your application grows. Service registrations, middleware configuration, authentication setup, and database connections all pile up, making the main entry point difficult to read and maintain.
The solution? WebApplicationBuilder extension methods. This architectural pattern helps you organize configuration into logical, reusable modules while keeping your Program.cs
clean and focused.
Table of Contents
Open Table of Contents
The Problem: Bloated Program.cs
In a typical ASP.NET Core application, your Program.cs
might look like this:
var builder = WebApplication.CreateBuilder(args);
// Database configuration
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
// Authentication
builder.Services.AddIdentity<User, Role>()
.AddEntityFrameworkStores<AppDbContext>();
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
// JWT configuration...
});
// CORS
builder.Services.AddCors(options => {
// CORS configuration...
});
// Controllers and API
builder.Services.AddControllers();
builder.Services.AddSwaggerGen();
// Business services
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();
// Validation
builder.Services.AddFluentValidation();
// Logging
builder.Host.UseSerilog();
var app = builder.Build();
// Configure middleware pipeline...
As your application grows, this file becomes increasingly difficult to maintain, test, and understand.
The Solution: WebApplicationBuilder Extensions
The elegant solution is to create extension methods that encapsulate related configuration concerns. Here’s how you can transform the above code:
Step 1: Create the Extension Class
Create a WebApplicationBuilderExtensions.cs
file:
namespace YourApp.Extensions;
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder ConfigureAllFeatures(this WebApplicationBuilder builder)
{
return builder
.ConfigureApi()
.ConfigureAuthentication()
.ConfigureBusiness()
.ConfigureCorsPolicy()
.ConfigureData()
.ConfigureErrorHandling()
.ConfigureLogging()
.ConfigureSettings()
.ConfigureValidation();
}
}
Step 2: Implement Individual Configuration Methods
Each method handles a specific concern:
Data Configuration
public static WebApplicationBuilder ConfigureData(this WebApplicationBuilder builder)
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
{
#if DEBUG
options.UseSqlServer(connectionString).EnableSensitiveDataLogging();
#else
options.UseSqlServer(connectionString);
#endif
options.ConfigureWarnings(warnings =>
warnings.Ignore(RelationalEventId.BoolWithDefaultWarning));
}, ServiceLifetime.Transient);
builder.Services.AddDataRepositories();
return builder;
}
API Configuration
public static WebApplicationBuilder ConfigureApi(this WebApplicationBuilder builder)
{
builder.Services.AddControllers().AddJsonOptions(opts =>
{
var enumConverter = new JsonStringEnumConverter();
opts.JsonSerializerOptions.Converters.Add(enumConverter);
opts.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});
builder.Services.AddSwaggerGen();
builder.Services.ConfigureOptions<ConfigureSwaggerGenOptions>();
builder.Services.ConfigureOptions<ConfigureSwaggerUIOptions>();
return builder;
}
Authentication Configuration
public static WebApplicationBuilder ConfigureAuthentication(this WebApplicationBuilder builder)
{
builder.Services.AddIdentityCore<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = true;
options.User.RequireUniqueEmail = true;
options.User.AllowedUserNameCharacters =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
})
.AddRoles<ApplicationRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
builder.Services.Configure<JwtAuthOptions>(
builder.Configuration.GetSection(JwtAuthOptions.SectionName));
var jwtAuthOptions = builder.Configuration
.GetSection(JwtAuthOptions.SectionName).Get<JwtAuthOptions>()!;
builder.Services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = jwtAuthOptions.Issuer,
ValidAudiences = jwtAuthOptions.Audiences,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtAuthOptions.Key)),
};
});
builder.Services.AddAuthorization();
return builder;
}
Business Services Configuration
public static WebApplicationBuilder ConfigureBusiness(this WebApplicationBuilder builder)
{
builder.Services.AddHttpContextAccessor();
builder.Services.AddTransient<TokenProvider>();
builder.Services.AddScoped<UserContext>();
builder.Services.AddBusinessServices();
builder.Services.AddLocalization();
builder.Services.AddMemoryCacheProvider();
builder.Services.AddEventHandlerProvider();
return builder;
}
Error Handling Configuration
public static WebApplicationBuilder ConfigureErrorHandling(this WebApplicationBuilder builder)
{
builder.Services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Extensions.TryAdd("requestId",
context.HttpContext.TraceIdentifier);
};
});
builder.Services.AddExceptionHandler<ValidationExceptionHandler>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
return builder;
}
Step 3: Clean Program.cs
Now your Program.cs
becomes beautifully simple:
using YourApp.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Configure all features with a single method call
builder.ConfigureAllFeatures();
var app = builder.Build();
// Configure middleware pipeline
app.UseExceptionHandler();
app.UseCors(CorsOptions.PolicyName);
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Benefits of This Approach
1. Separation of Concerns
Each extension method handles a specific aspect of configuration, making the codebase more organized and maintainable.
2. Improved Readability
Your Program.cs
becomes a high-level overview of your application’s architecture, making it easier for new team members to understand.
3. Reusability
Extension methods can be reused across different projects or environments with minimal changes.
4. Testability
Individual configuration methods can be unit tested in isolation, improving your test coverage.
5. Modularity
You can easily enable or disable features by commenting out a single line in ConfigureAllFeatures()
.
6. Environment-Specific Configuration
Each method can include environment-specific logic without cluttering the main entry point.
Advanced Patterns
Conditional Configuration
You can make configuration conditional based on environment or feature flags:
public static WebApplicationBuilder ConfigureAllFeatures(this WebApplicationBuilder builder)
{
return builder
.ConfigureApi()
.ConfigureAuthentication()
.ConfigureBusiness()
.ConfigureData()
.ConfigureConditionally(builder.Environment.IsDevelopment(),
b => b.ConfigureDevelopmentFeatures())
.ConfigureConditionally(builder.Configuration.GetValue<bool>("Features:EnableCaching"),
b => b.ConfigureCaching());
}
private static WebApplicationBuilder ConfigureConditionally(
this WebApplicationBuilder builder,
bool condition,
Func<WebApplicationBuilder, WebApplicationBuilder> configure)
{
return condition ? configure(builder) : builder;
}
Configuration Validation
Add validation to ensure required configuration is present:
public static WebApplicationBuilder ConfigureData(this WebApplicationBuilder builder)
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrEmpty(connectionString))
{
throw new InvalidOperationException(
"Connection string 'DefaultConnection' not found. " +
"Please check your appsettings.json configuration.");
}
// Rest of configuration...
return builder;
}
Feature Flags Integration
Integrate with feature management libraries:
public static WebApplicationBuilder ConfigureFeatures(this WebApplicationBuilder builder)
{
builder.Services.AddFeatureManagement();
// Conditionally register services based on feature flags
if (builder.Configuration.GetValue<bool>("Features:EnableAdvancedLogging"))
{
builder.ConfigureAdvancedLogging();
}
return builder;
}
Best Practices
-
Keep methods focused: Each extension method should handle one logical area of configuration.
-
Use meaningful names: Method names should clearly indicate what they configure.
-
Return the builder: Always return the
WebApplicationBuilder
to enable method chaining. -
Handle errors gracefully: Include proper error handling and validation.
-
Document dependencies: Use XML comments to document any configuration dependencies.
-
Consider order: Some configurations depend on others, so order matters in
ConfigureAllFeatures()
. -
Use the Options pattern: Leverage ASP.NET Core’s configuration and options patterns for type-safe configuration.
Real-World Example Structure
Here’s how you might organize a complex application:
public static class WebApplicationBuilderExtensions
{
public static WebApplicationBuilder ConfigureAllFeatures(this WebApplicationBuilder builder)
{
return builder
.ConfigureLogging() // Logging
.ConfigureSettings() // Load and validate configuration
.ConfigureData() // Database and data access
.ConfigureAuthentication() // Identity and JWT
.ConfigureAuthorization() // Policies and permissions
.ConfigureCorsPolicy() // Cross-origin requests
.ConfigureApi() // Controllers and Swagger
.ConfigureBusiness() // Business logic services
.ConfigureValidation() // Input validation
.ConfigureErrorHandling() // Exception handling
.ConfigureCaching() // Caching strategies
.ConfigureHealthChecks() // Health monitoring
.ConfigureBackgroundServices(); // Hosted services
}
}
Conclusion
Using WebApplicationBuilder extension methods is a simple yet powerful pattern that brings significant benefits to your ASP.NET Core applications. It promotes clean architecture, improves maintainability, and makes your applications more modular and testable.
By organizing your configuration into logical, focused extension methods, you create a more professional and maintainable codebase that scales well as your application grows. Your future self (and your teammates) will thank you for the improved clarity and organization.
Start refactoring your Program.cs
today, and experience the benefits of this clean architectural pattern!