Merging Startup.cs and Program.cs in .NET 8: A Simplified Approach
Published: 2024-07-02 13:38:05

As .NET evolves, so does the way we structure our applications. One notable change introduced in recent versions of .NET is the ability to merge `Startup.cs` and `Program.cs` into a single file. This approach streamlines the setup process, making it more cohesive and manageable. This article will discuss the rationale behind merging these files, walk through the process, and highlight potential advantages and pitfalls.

## Why Merge Startup.cs and Program.cs?
Okay, first let me try to find some advantages and also why it can be a bad idea.

### Advantages

1. **Simplified Structure**: By consolidating `Startup.cs` and `Program.cs`, you create a single entry point for application configuration. This can make the codebase more straightforward to navigate and understand. Updates to configuration or middleware can be made in one file rather than spread across multiple files, reducing the likelihood of inconsistencies and errors.

   
2. **Improved Testability**: With all configurations in one place, writing integration tests becomes simpler. Conditions and configurations are centralized, making it easier to mock dependencies and test different scenarios.

### Potential Pitfalls

1. **Complexity in Large Applications**: For very large applications, having all configurations in one file might become unwieldy. It's essential to balance simplicity with readability.
   
2. **Migration Challenges**: If you're transitioning an existing application, merging these files might introduce bugs if not done carefully. A rollback could be a nightmare (speaking from experience!).

## The Old Way: Separate Startup.cs and Program.cs

### Original Program.cs

```csharp
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Logging.ApplicationInsights;
using System.Diagnostics;

public class Program
{
    public static void Main(string[] args)
    {
        try
        {
            Debug.WriteLine("Configure infrastructure...");
            BuildHost(args).Run();
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Infrastructure configuration failed: {ex}");
        }
    }

    public static IHost BuildHost(string[] args)
    {
        // Configuration and host building logic
    }
}
```

### Original Startup.cs

```csharp
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.FeatureManagement;
using System.Diagnostics;

public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
    {
        Configuration = configuration;
        WebHostEnvironment = webHostEnvironment;
    }

    public IConfiguration Configuration { get; }
    public IWebHostEnvironment WebHostEnvironment { get; }

    // Methods for configuring services and middleware
}
```

## The New Combined Mode

### Combined Program.cs

```csharp
using Microsoft.AspNetCore.Http.Features;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
using Microsoft.FeatureManagement;
using Newtonsoft.Json.Converters;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// Configuration setup
// Using builder.Configuration to setup configuration sources
builder.Configuration.AddAzureAppConfiguration(options =>
{
    options.UseFeatureFlags(o =>
    {
        o.Label = "InstanceName"; // Replace with your instance name
        o.CacheExpirationInterval = TimeSpan.FromMinutes(10);
    });
    options.ConfigureKeyVault(o =>
    {
        o.SetCredential(new DefaultAzureCredential()); // Replace with your Azure credential
        o.SetSecretRefreshInterval(TimeSpan.FromMinutes(30));
    });
    options.Connect("YourAppConfigurationEndpoint", new DefaultAzureCredential()) // Replace with your endpoint and credential
        .ConfigureRefresh(o =>
        {
            o.Register("Settings:Sentinel", refreshAll: true)
               .SetCacheExpiration(TimeSpan.FromMinutes(10));
        })
        .Select(KeyFilter.Any, LabelFilter.Null)
        .Select(KeyFilter.Any, labelFilter: "InstanceName"); // Replace with your instance name
});

if (builder.Environment.IsDevelopment())
{
    builder.Configuration.AddJsonFile("appsettings.json", optional: true);
    builder.Configuration.AddUserSecrets<Program>();
}

builder.Configuration.AddEnvironmentVariables();

// Logging setup
// Using builder.Logging to setup logging providers
builder.Logging.ClearProviders(); // Clear default providers
builder.Logging.AddConsole(); // Add console logging
builder.Logging.AddDebug(); // Add debug logging
builder.Logging.AddAzureWebAppDiagnostics(); // Add Azure diagnostics

if (!builder.Environment.IsDevelopment() && Environment.GetEnvironmentVariable("AUTOMATED_TESTING") is null)
{
    builder.Logging.AddApplicationInsights(config =>
    {
        config.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
        config.DisableTelemetry = false;
    }, options => options.IncludeScopes = false);
}

// Services setup
var services = builder.Services;
services.AddControllers().AddNewtonsoftJson(opt => opt.SerializerSettings.Converters.Add(new StringEnumConverter()));
services.AddDbContext<YourDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
services.AddFeatureManagement();
services.AddAzureAppConfiguration();
// Register other services as needed

// Middleware setup
var app = builder.Build();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

// Utility methods for retrieving services
private static T GetService<T>(IServiceCollection services)
{
    ServiceProvider serviceProvider = services.BuildServiceProvider();
    return serviceProvider.GetService<T>() ?? throw new Exception($"Could not find service {typeof(T)}");
}

private static T GetService<T>(IApplicationBuilder app)
{
    return app.ApplicationServices.GetService<T>() ?? throw new Exception($"Could not find service {typeof(T)}");
}

private static void DebugWrite(string message)
{
    Console.WriteLine(message);
    Debug.WriteLine(message);
}
```

### Key Points in the Combined File

- **Configuration**: Configuration sources are added using `builder.Configuration`. This includes Azure App Configuration, JSON files, user secrets, and environment variables.
- **Logging**: Logging is set up using `builder.Logging` with different providers for console, debug, and Application Insights. The `builder.Logging` API simplifies logging configuration by providing a centralized way to add and configure logging providers.
- **Services**: All service configurations, including custom services, middleware, and feature management, are consolidated. Using `builder.Services` makes it straightforward to register services with the dependency injection container.
- **Middleware**: Middleware components are configured in one place, improving readability and maintainability. The `app.UseRouting()`, `app.UseAuthentication()`, and `app.UseAuthorization()` methods set up the middleware pipeline.

### Our Experience and story
In our project, we have more than 100 active production instances, each with unique configuration and settings. 

We faced numerous conditions for integration tests, such as checking `Environment.GetEnvironmentVariable("AUTOMATED_TESTING")` or custom feature flag conditions based on license. 

All those conditions determine whether the application should use a different database or configure other testing-specific settings. And managing these conditions across multiple files was a pain in the a. 

Since merging `Startup.cs` and `Program.cs`, we have gained a much nicer overview and more control over our application. The centralized configuration has made it significantly easier to maintain and extend our application, particularly when writing and running integration tests and switching custom features.


### Conclusion

Merging `Startup.cs` and `Program.cs` can streamline your .NET applications, making them easier to test and maintain. However, be cautious during the transition to avoid introducing bugs. Start by merging the files as they are before making any improvements. This way, if something goes wrong, you'll have an easier time debugging (trust me, it happened to me...).

By following the steps and best practices outlined in this article, you can take advantage of the modern .NET hosting model and simplify your application's setup.