In my previous post, I showed how you could configure different services for dependency injection depending on the current hosting environment, i.e. whether you are currently running in Development or Production.
The approach I demonstrated required storing the IHostingEnvironment
variable passed to the constructor of your Startup
class, for use in the ConfigureServices
method.
In this post I show an alternative approach, in which you use environment-specific methods on your startup class, rather than if-else statements in ConfigureServices
.
The default Startup class
By convention, a standard ASP.NET Core application uses a Startup
class for application setting configuration, setting up dependency injection, and defining the middleware pipeline. If you use the default MVC template when creating your project, this will produce a Startup
class with a signature similar to the following:
public class Startup
{
public Startup(IHostingEnvironment env)
{
// Configuration settings
}
public void ConfigureServices(IServiceCollection services)
{
// Service dependency injection configuration
}
public void Configure(IApplicationBuilder app)
{
/// Middleware configuration
}
}
Note that the Startup
class does not implement any particular interface, or inherit from a base class. Instead, it is a simple class, which follows naming conventions for the configuration methods.
As a reminder, this class is referenced as part of the WebHostBuilder
setup, which typically resides in Program.cs:
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
host.Run();
In addition to this standard format, there are some supplementary conventions that are particularly useful in the scenarios I described in my last post, where you want a different service injected depending on the runtime hosting environment. I'll come to the conventions shortly, but for now consider the following scenario.
I have an ISmsService
my application uses to send SMS messages. In production I will need to use the full implementation, but when in development I don't want an SMS to be sent every time I test it, especially as it costs me money each time I use it. Instead, I need to use a dummy implementation of ISmsService
.
Extension methods
In the previous post, I showed how you can easily use an if-else construct in your ConfigureServices
method to meet these requirements. However, if you have a lot of these dummy services that need to be wired up, the method could quickly become long and confusing, especially in large apps with a lot of dependencies.
This can be mitigated to an extent if you use the extension method approach for configuring your internal services, similar to that suggested by K. Scott Allen in his post. In his post, he suggests wrapping each segment of configuration in an extension method, to keep the Startup.ConfigureServices
method simple and declarative.
Considering the SMS example above, we might construct an extension method that takes in the hosting environment, and configures all the ancillary services. For example:
public static class SmsServiceExtensions
{
public static IServiceCollection AddSmsService(this IServiceCollection services, IHostingEnvironment env, IConfigurationRoot config)
{
services.Configure<SmsSettings>(config.GetSection("SmsSettings"));
services.AddSingleton<ISmsTemplateFactory, SmsTemplateFactory>();
if(env.IsDevelopment())
{
services.AddTransient<ISmsService, DummySmsService>();
}
else
{
services.AddTransient<ISmsService, SmsService>();
}
return services;
}
}
These extension methods encapsulate a discrete unit of configuration, all of which would otherwise have resided in the Startup.ConfigureServices
method, leaving your ConfigureServices
method far easier to read:
public void ConfigureServices(IServiceCollection services)
{
services.AddMVC();
services.AddSmsService(Environment, Configuration);
}
The downside to this approach is that your service configuration is now spread across many different classes and methods. Some people will prefer to have all the configuration code in the Startup.cs file, but still want to avoid the many if-else constructs for configuring Development vs Production dependencies.
Luckily, there is another approach at your disposal, by way of environment-specific Configure
methods.
Environment-Specific method conventions
The ASP.NET Core WebHostBuilder
has a number of conventions it follows when locating the configuration methods on the Startup
class.
As we've seen, the Configure
and ConfigureServices
methods are used by default. The main advantage of not requiring an explicit interface implementation is that the WebHostBuilder
can inject additional dependencies into these methods. However it also enables the selection of different methods depending on context.
As described in the documentation, the Startup
class can contain environment specific configuration methods of the form Configure{EnvironmentName}()
and Configure{EnvironmentName}Services()
.
If the WebHostBuilder
detects methods of this form, they will be called preferentially to the standard Configure
and ConfigureServices
methods. We can use this to avoid the proliferation of if-else in our startup class. For example, considering the SMS configuration previously:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
ConfigureCommonServices(services);
services.AddTransient<ISmsService, SmsService>();
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureDevelopmentServices(IServiceCollection services)
{
ConfigureCommonServices(services);
services.AddTransient<ISmsService, DummySmsService>();
}
private void ConfigureCommonServices(IServiceCollection services)
{
services.Configure<SmsSettings>(config.GetSection("SmsSettings"));
services.AddSingleton<ISmsTemplateFactory, SmsTemplateFactory>();
}
}
With this approach, we can just configure our alternative implementation services in the appropriate methods. At runtime the WebHostBuilder
will check for the presence of a Configure{EnvironmentName}Services
method.
When running in Development, ConfigureDevelopmentServices
will be selected and the DummySmsService
will be used. In any other environment, the default ConfigureServices
will be called.
Note that we add all the service configuration that is common between environments to a private method ConfigureCommonServices
, which is called by both configure methods. This prevents fragile duplication of configuration for services common between environments.
Environment-Specific class conventions
As well as the convention based methods in Startup
, you can also take a convention-based approach for the whole Startup
class. This allows you to completely separate your configuration code when in Development from other environments, by creating classes of the form Startup{Environment}
.
For example, you can create a StartupDevelopment
class and a Startup
class - when you run in the Development environment, StartupDevelopment
will be used for configuring your app and services. In other environments, Startup
will be used.
So for example, we could have the following Startup
Class
public class Startup
{
public Startup(IHostingEnvironment env)
{
// Configuration settings
}
public void ConfigureServices(IServiceCollection services)
{
// Service dependency injection configuration
services.AddTransient<ISmsService, SmsService>();
}
public void Configure(IApplicationBuilder app)
{
/// Middleware configuration
}
}
and an environment-specific version for the Development environment:
public class StartupDevelopment
{
public StartupDevelopment(IHostingEnvironment env)
{
// Configuration settings
}
public void ConfigureServices(IServiceCollection services)
{
// Service dependency injection configuration
services.AddTransient<ISmsService, DummySmsService>();
}
public void Configure(IApplicationBuilder app)
{
/// Middleware configuration
}
}
In order to use this convention based approached to your startup class, you need to use a different overload in the WebHostBuilder
:
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup(assemblyName)
.Build();
host.Run();
Rather than using the generic UseStartup<T>
method, we need to use the overload UseStartup(string startupAssemblyName)
. Under the hood, the WebHostBuilder
will use reflection to find a Startup class in the provided assembly called Startup
or Startup{Environment}
. The environment-specific class will be used by default, falling back to the Startup
class if no environment-specific version is found. If no candidate classes are found, the builder will throw an InvalidOperationException
when starting your application.
Bear in mind that if you use this approach, you will need to duplicate any configuration that is common between environments, including application settings, service configuration and the middleware pipeline. If your application runs significantly differently between Development and Production then this approach may work best for you.
In my experience, the majority of the application configuration is common between all environments, with the exception of a handful of environment-specific services and middleware. Generally I prefer the if-else approach with encapsulation via extension methods as the application grows, but it is generally down to personal preference, and what works best for you.
Summary
In a previous post, I showed how you could use IHostingEnvironment
to control which services are registered with the DI container at runtime, depending on the hosting environment.
In this post, I showed how you could achieve a similar result using naming conventions baked in to the WebHostBuilder
implementation.
These conventions allow automatic selection of Configure{EnvironmentName}
and Configure{Environment}Services
methods in your Startup
class depending on the current hosting environment.
Additionally, I showed the convention based approach to Startup
class selection, whereby your application will automatically select a Startup class of the form Startup{Environment}
if available.