X

ASP.NET Core: Environment based start-up classes

My previous blog post ASP.NET Core: Environment based configuring methods introduced how to use environment based configuring methods of application start-up class. This blog post goes further and introduces environment based start-up classes that can be used by applications that have more complex needs on configuration based on environment.

But what if start-up class grows too big if it has environment based configuring methods added? Or what about more complex cases where configuring means some complex code that heavily depends on environments where application runs? Using configuring methods we will end up with too big or too complex class that we need to split smaller.

Getting ready for multiple start-up classes

Start-up class supports the same naming style as configuring methods in my previous post ASP.NET Core: Environment based configuring methods. We can create start-up classes ending with environment name. By example:

  • StartupDevelopment
  • StartupProduction

Like with configuring methods we leave the default one (here the original start-up class) for environments that doesn’t have or doesn’t need special configuration.

There’s one thing we have to change – call to UseStartup() methods in Program class.

public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration() // IMPORTANT!!!
            .UseApplicationInsights()

            .UseStartup(Assembly.GetEntryAssembly().FullName)
            .Build();

        host.Run();
    }
}

NB! Generic version of UseStartup() method uses start-up class of given type and it doesn’t consider environment based start-up classes.

Adding environment based start-up classes

Now let’s add new start-up class StartupDevelopment to our project. I modified it to meet the needs of my prototype application. Go through the code quickly and see what’s done there.

public class StartupDevelopment
{
    public IConfigurationRoot Configuration { get; }

    public StartupDevelopment(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .AddJsonFile("appsettings.json")
                            .SetBasePath(env.ContentRootPath)
                            .AddEnvironmentVariables();

        builder.AddUserSecrets<Startup>();
        builder.AddApplicationInsightsSettings(developerMode: true);

        Configuration = builder.Build();
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLocalization();
        services.AddMvc();

        services.Configure<RouteOptions>(options =>
        {
            options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint));
        });

        services.AddScoped<AtlasClient>();
        services.AddScoped<ErrLocalizer>();
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();
        loggerFactory.AddFile("wwwroot/logs/ts-{Date}.txt");

        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();

        app.UseStaticFiles();

        var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
        app.UseRequestLocalization(options.Value);
        app.UseResponseCaching();

        app.UseMvc(routes =>
        {


            routes.MapRoute(
                name: "LocalizedDefault",
                template: "{culture:lang}/{controller=Home}/{action=Index}/{id?}",
                defaults: ""
            );

            routes.MapRoute(
                name: "default",
                template: "{*catchall}",
                defaults: new { controller = "Home", action = "RedirectToDefaultLanguage", culture = "et" });
        });
    }
}

We can put breakpoint to somewhere in this class and run application in Visual Studio to see that StartupDevelopment class is used instead of default Startup class.

Avoiding duplicated code

Although it is easy to add new start-up class, fill it with code from other similar class and make modifications it leads us to duplicated code. Adding routes to MVC is good example of duplicated code. We can solve this problem to some point if environment based classes inherit from default one.

Some things I want to mention before getting to code:

  • Middleware may depend on ordering
  • I prefer to leave MVC related stuff as last one in the line
  • Adding virtual methods for special cases is dead end street because code gets harder and harder to handle
  • Framework level dependency injection mappings may need separate virtual method

With these considerations in mind let’s modify Startup, StartupDevelopment and StartupProduction classes.

public class Startup
{
    public IConfigurationRoot Configuration { get; }

    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .AddJsonFile("appsettings.json")
                            .SetBasePath(env.ContentRootPath)
                            .AddEnvironmentVariables();

        ConfigureBuilder(builder);

        Configuration = builder.Build();
    }

    public virtual void ConfigureBuilder(IConfigurationBuilder builder)
    {
    }

    public virtual void ConfigureServices(IServiceCollection services)
    {

        services.AddLocalization();

        services.AddMvc();

        services.Configure<RouteOptions>(options =>
        {
            options.ConstraintMap.Add("lang", typeof(LanguageRouteConstraint));
        });

        ConfigureDependencyInjection(services);
    }

    public virtual void ConfigureDependencyInjection(IServiceCollection services)
    {
        services.AddScoped<IMailClient, MailClient>();
        services.AddScoped<AtlasClient>();
        services.AddScoped<ErrLocalizer>();
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    }

    public virtual void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddFile("wwwroot/logs/ts-{Date}.txt");           

        app.UseStaticFiles();

        var options = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
        app.UseRequestLocalization(options.Value);


        app.UseMvc(routes =>
        {

            routes.MapRoute(
                name: "LocalizedDefault",
                template: "{culture:lang}/{controller=Home}/{action=Index}/{id?}",
                defaults: ""
            );

            routes.MapRoute(
                name: "default",
                template: "{*catchall}",
                defaults: new { controller = "Home", action = "RedirectToDefaultLanguage", culture="et" });
        });           
    }
}

public class StartupDevelopment : Startup
{
    public StartupDevelopment(IHostingEnvironment env) : base(env)
    {
    }

    public override void ConfigureBuilder(IConfigurationBuilder builder)
    {
        builder.AddUserSecrets<Startup>();
        builder.AddApplicationInsightsSettings(developerMode: true);

        base.ConfigureBuilder(builder);
    }

    public override void ConfigureDependencyInjection(IServiceCollection services)
    {
        base.ConfigureDependencyInjection(services);

        services.AddScoped<IMailClient, FakeMailClient>();
    }

    public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddDebug();

        app.UseDeveloperExceptionPage();
        app.UseBrowserLink();

        base.Configure(app, env, loggerFactory);
    }
}

public class StartupProduction : Startup
{
    public StartupProduction(IHostingEnvironment host) : base(host)
    {
    }

    public override void ConfigureServices(IServiceCollection services)
    {
        services.AddApplicationInsightsTelemetry(Configuration);

        base.ConfigureServices(services);
    }

    public override void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        app.UseExceptionHandler("/Home/Error");

        if (Configuration.GetValue("UseForwardedHeaders", false))
        {
            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });
        }

        base.Configure(app, env, loggerFactory);
    }
}

Take a note how we configure dependency injection in development start-up. First we call base method and then we add our own implementations. This is because defining dependencies is done using last wins model. It means that instance type used is the one that was introduced last.

Wrapping up

Environment based start-up classes are powerful feature of ASP.NET Core. Using these classes with more complex applications let’s us separate environment based settings to separate classes and achieve cleaner code this way. This strategy can get tricky as some middleware services may depend on order they are added to services collection and in these cases we have to find some more flexible solution to keep code clean and simple. Still using tricks shown here may be of great help when building configuration classes.

Liked this post? Empower your friends by sharing it!
Categories: ASP.NET

View Comments (2)

Related Post