How To Create Environment Specific Startup Class In .NET

How To Create Environment Specific Startup Class In .NET

In previous two articles, we have seen how to set the environment variables and how the environment variables play role in config transformations. In this article, let’s have a look at how environment specific startup classes can be used in .NET web application.

Why ?

Every software application goes through various stages before reaching the production. The software that you are developing is on development environment during development, it might be on staging / test environments (one or more) during its testing and then it is deployed to production environment, where the real intended users can access the application.

The software might be interacting with external applications or services. The word external does not necessarily mean applications or services from outside the organization which owns the software. It may sometimes means other software components owned by same organization.

For example, a B2C application might have a phone app too, both of the UI apps interacting with Web APIs. And every component being developed by separate teams.

So, some of these services might not be available during development or testing. Or sometimes they are not accessible, or have limited access because of various reasons.

Also, some technical requirements might not be required on certain environments. e.g. generally, you may not want to enable DDos protection on development environment.

OR, some services like payment gateways, or government identity services might not be available for dev and test environments (or if they are available, they might have limited availability due to cost associated with it.).

So, in other words, the request processing pipeline setup might be different on different environments. Also, some environments might use stubs or mocks in place of calling actual services.

The startup.cs is the file where you can configure request processing pipeline and other services required by your .NET web application.

Simplest Way

If the application startup for various environments does not differ largely and there are some minor things here and there, then the simplest method is to add conditions in the startup as shown in below code.

For this approach, you need to inject IWebHostEnvironment into the Startup constructor. This class will expose methods to IsEnvironment(IHostEnvironment, String), along with other 3 methods to check the current environment name

Also, have a look at UseStartup overload, which in this case, uses generic type, as parameter.

For ex. Let’s have a look at the below code. It configures the exception handler for the MVC application, ensuring that server exception details are shown only on development environment. On all other environments, a custom error page would be shown. This is achieved by simple IF condition.

// Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// Notice the UseStartup overload specifying the class name
webBuilder.UseStartup<Startup>();
});
}
// Startup.cs
public class Startup
{
private readonly IWebHostEnvironment env;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
this.env = env;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
if(this.env.IsEnvironment("Security"))
{
// Setup services for security environment
}
else if(this.env.IsDevelopment())
{
// Setup stubs for external services
}
else
{
// Setup services required for other environments
}
// common services
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
view raw Startup.cs hosted with ❤ by GitHub

Similarly, you can use conditions to register dependency injection for interfaces to ensure that stubs are loaded for external services if the application is running on development environment.

Probable Issue

The issue with above mentioned approach is very obvious.

If the variations in the startups on every environments are very huge, then soon your startup logic will contain a lot of conditions, hard to read/perceive and hard to maintain too in many cases.

Solution – Environment Specific Startups

In order to reduce cyclomatic complexity (which increases due to multiple and/or nested IF statements), you can consider having environment specific startup file.

Before deciding, you must check two things

  • How many IF…ELSE flows would be avoided.
  • How to organize common startup code which is required for all environments

Some code duplication might happen if you choose to have environment specific startups classes. For ex, every file will have middleware to serve static files or handle exceptions lets say. Therefore, you should carefully design how this code should be organized to avoid redundancy and to keep the solution maintainable.

How does it work ?

There is a clue in the title, did you guess it ? Obviously, using ASPNETCORE_ENVIRONMENT variable.

The .NET application uses Startup class for bootstrapping the application. The application can define one class for all environments or multiple Startup classes for different environments.

The runtime uses below logic to select appropriate Startup class:

  • If the class with name Startup{EnvironmentName} is present, then it is used for startup.
  • If there is no startup class with current environment name as Suffix, then the regular Startup class is used by runtime to bootstrap the solution.

Also, here, in below sample, we are using UseStartup overload which accepts assemblyName parameter. This is the FullName of assembly, which contains all the Startup classes.

// Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args)
{
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
// Notice the UseStartup overload with assemblyName parameter.
webBuilder.UseStartup(assemblyName);
});
}
}
// StartupProduction.cs – Production environment startup
public class StartupProduction
{
public StartupProduction(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Register services required for production environment
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
// Startup.cs – for all other environments
public class Startup
{
private readonly IWebHostEnvironment env;
public Startup(IConfiguration configuration, IWebHostEnvironment env)
{
Configuration = configuration;
this.env = env;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
if (this.env.IsEnvironment("Security"))
{
// Setup services for security environment
}
else if (this.env.IsDevelopment())
{
// Setup stubs for external services
}
else
{
// Setup services required for other environments
}
// common services
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
view raw Startup.cs hosted with ❤ by GitHub

As said earlier, this is preferable only if the bootstrapping logic is completely different for some environments from other environments. Typically you may not need this but just in case you are worrying about a lot of conditional logic on startup, this may be a way to go.

Frankly speaking, I have not seen (YET) a real world application which needs this feature. Have you used this feature in any application ? OR are you planning to use it in near future ? Let me know your thoughts.

Leave a Reply

This Post Has One Comment