Skip to content
Go back

Organizing ASP.NET Configuration with WebApplicationBuilder Extensions

Published:

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

  1. Keep methods focused: Each extension method should handle one logical area of configuration.

  2. Use meaningful names: Method names should clearly indicate what they configure.

  3. Return the builder: Always return the WebApplicationBuilder to enable method chaining.

  4. Handle errors gracefully: Include proper error handling and validation.

  5. Document dependencies: Use XML comments to document any configuration dependencies.

  6. Consider order: Some configurations depend on others, so order matters in ConfigureAllFeatures().

  7. 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!



Next Post
Good Practices in C# Web APIs: Avoiding Common Pitfalls