02 July 2024
by
Hung Tran – Webscope.io
Tran Manh Hung

Merging Startup.cs and Program.cs in .NET 8: A Simplified Approach

.NET
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!).
Banner

Do you need a reliable partner in tech for your next project?

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

Original Program.cs

1using Microsoft.Extensions.Configuration.AzureAppConfiguration;
2using Microsoft.Extensions.Logging.ApplicationInsights;
3using System.Diagnostics;
4
5public class Program
6{
7    public static void Main(string[] args)
8    {
9        try
10        {
11            Debug.WriteLine("Configure infrastructure...");
12            BuildHost(args).Run();
13        }
14        catch (Exception ex)
15        {
16            Debug.WriteLine($"Infrastructure configuration failed: {ex}");
17        }
18    }
19
20    public static IHost BuildHost(string[] args)
21    {
22        // Configuration and host building logic
23    }
24}

Original Startup.cs

1using Microsoft.AspNetCore.Http.Features;
2using Microsoft.EntityFrameworkCore;
3using Microsoft.Extensions.Options;
4using Microsoft.FeatureManagement;
5using System.Diagnostics;
6
7public class Startup
8{
9    public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
10    {
11        Configuration = configuration;
12        WebHostEnvironment = webHostEnvironment;
13    }
14
15    public IConfiguration Configuration { get; }
16    public IWebHostEnvironment WebHostEnvironment { get; }
17
18    // Methods for configuring services and middleware
19}

The New Combined Mode

Combined Program.cs

1using Microsoft.AspNetCore.Http.Features;
2using Microsoft.EntityFrameworkCore;
3using Microsoft.Extensions.Configuration.AzureAppConfiguration;
4using Microsoft.Extensions.Logging;
5using Microsoft.Extensions.Logging.ApplicationInsights;
6using Microsoft.FeatureManagement;
7using Newtonsoft.Json.Converters;
8using System.Diagnostics;
9
10var builder = WebApplication.CreateBuilder(args);
11
12// Configuration setup
13// Using builder.Configuration to setup configuration sources
14builder.Configuration.AddAzureAppConfiguration(options =>
15{
16    options.UseFeatureFlags(o =>
17    {
18        o.Label = "InstanceName"; // Replace with your instance name
19        o.CacheExpirationInterval = TimeSpan.FromMinutes(10);
20    });
21    options.ConfigureKeyVault(o =>
22    {
23        o.SetCredential(new DefaultAzureCredential()); // Replace with your Azure credential
24        o.SetSecretRefreshInterval(TimeSpan.FromMinutes(30));
25    });
26    options.Connect("YourAppConfigurationEndpoint", new DefaultAzureCredential()) // Replace with your endpoint and credential
27        .ConfigureRefresh(o =>
28        {
29            o.Register("Settings:Sentinel", refreshAll: true)
30               .SetCacheExpiration(TimeSpan.FromMinutes(10));
31        })
32        .Select(KeyFilter.Any, LabelFilter.Null)
33        .Select(KeyFilter.Any, labelFilter: "InstanceName"); // Replace with your instance name
34});
35
36if (builder.Environment.IsDevelopment())
37{
38    builder.Configuration.AddJsonFile("appsettings.json", optional: true);
39    builder.Configuration.AddUserSecrets<Program>();
40}
41
42builder.Configuration.AddEnvironmentVariables();
43
44// Logging setup
45// Using builder.Logging to setup logging providers
46builder.Logging.ClearProviders(); // Clear default providers
47builder.Logging.AddConsole(); // Add console logging
48builder.Logging.AddDebug(); // Add debug logging
49builder.Logging.AddAzureWebAppDiagnostics(); // Add Azure diagnostics
50
51if (!builder.Environment.IsDevelopment() && Environment.GetEnvironmentVariable("AUTOMATED_TESTING") is null)
52{
53    builder.Logging.AddApplicationInsights(config =>
54    {
55        config.ConnectionString = builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"];
56        config.DisableTelemetry = false;
57    }, options => options.IncludeScopes = false);
58}
59
60// Services setup
61var services = builder.Services;
62services.AddControllers().AddNewtonsoftJson(opt => opt.SerializerSettings.Converters.Add(new StringEnumConverter()));
63services.AddDbContext<YourDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
64services.AddFeatureManagement();
65services.AddAzureAppConfiguration();
66// Register other services as needed
67
68// Middleware setup
69var app = builder.Build();
70
71app.UseRouting();
72app.UseAuthentication();
73app.UseAuthorization();
74app.MapControllers();
75
76app.Run();
77
78// Utility methods for retrieving services
79private static T GetService<T>(IServiceCollection services)
80{
81    ServiceProvider serviceProvider = services.BuildServiceProvider();
82    return serviceProvider.GetService<T>() ?? throw new Exception($"Could not find service {typeof(T)}");
83}
84
85private static T GetService<T>(IApplicationBuilder app)
86{
87    return app.ApplicationServices.GetService<T>() ?? throw new Exception($"Could not find service {typeof(T)}");
88}
89
90private static void DebugWrite(string message)
91{
92    Console.WriteLine(message);
93    Debug.WriteLine(message);
94}

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.
Share post

Let’s stay connected

Do you want the latest and greatest from our blog straight to your inbox? Chuck us your email address and get informed.
You can unsubscribe any time. For more details, review our privacy policy