Testavior icon indicating copy to clipboard operation
Testavior copied to clipboard

The WebHostBuilder 2.0 causes all kinds of problems :(

Open snebjorn opened this issue 8 years ago • 2 comments

The new recommend way to start an asp net core 2.0 app is as follows

public static IWebHost BuildWebHost(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .UseStartup<Startup>()
            .Build();

If we take a look at CreateDefaultBuilder then we get

public static IWebHostBuilder CreateDefaultBuilder(string[] args)
{
    var builder = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            var env = hostingContext.HostingEnvironment;

            config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                    .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

            if (env.IsDevelopment())
            {
                var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                if (appAssembly != null)
                {
                    config.AddUserSecrets(appAssembly, optional: true);
                }
            }

            config.AddEnvironmentVariables();

            if (args != null)
            {
                config.AddCommandLine(args);
            }
        })
        .ConfigureLogging((hostingContext, logging) =>
        {
            logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
            logging.AddConsole();
            logging.AddDebug();
        })
        .UseIISIntegration()
        .UseDefaultServiceProvider((context, options) =>
        {
            options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
        });

    return builder;
}

and Startup is now expected to look like this

public class Startup
{
    public Startup(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)
    {
        services.AddMvc();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }

        app.UseMvc();
    }
}

Which causes problems for the TestEnvironment. Because the app is being setup outside Startup. Startup just adds a little flavor to the configuration it no longer creates/controls it.

Inside TestEnvironment we find CreateTestServer

protected virtual TestServer CreateTestServer()
{
    return new TestServer
    (
        new WebHostBuilder()
                .ConfigureStartup(new TStartupConfigurationService(), this.contentRootPath)
                .UseStartup<TStartup>()
    );
}

The problem with this is that the new WebHostBuilder() should reflect the configuration inside Main.BuildWebHost().

I tried solving this by deriving from TestEnvironment

public class MyTestEnviornment<TStartup, TStartupConfigurationService> : TestEnvironment<TStartup, TStartupConfigurationService>
{
    public MyTestEnviornment(string targetProjectRelativePath = null) : base(targetProjectRelativePath)
    {
    }

    protected override TestServer CreateTestServer()
    {
        return new TestServer
        (
            new WebHostBuilder()
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                    var env = hostingContext.HostingEnvironment;
                    env.EnvironmentName = "Test";

                    config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                        .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

                    if (env.IsDevelopment())
                    {
                        var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
                        if (appAssembly != null)
                        {
                            config.AddUserSecrets(appAssembly, optional: true);
                        }
                    }

                    config.AddEnvironmentVariables();
                })
                .ConfigureStartup(new TStartupConfigurationService(), this.contentRootPath)
                .UseStartup<TStartup>()
        );
    }
}

But that has a bunch of problems. this.contentRootPath is private and therefore inaccessible to MyTestEnviornment. Also note I have to do env.EnvironmentName = "Test"; because the below haven't been called yet.

// defined in IStartupConfigurationService
public void ConfigureEnvironment(IHostingEnvironment env)
{
    env.EnvironmentName = "Test";
}

I haven't figured out a good way of solving this. We need a way to control what gets added to WebHostBuilder. So a wrapper comes to mind, but then the nice "add-in" feeling disappears :(

snebjorn avatar Oct 11 '17 14:10 snebjorn

Hello @snebjorn, thanks for this information. I need to do several tests but for now you can just use the old way to initialize the WebHostBuilder, we still use it on our ASP.NET Core 2.0 apps and it works well. I'll keep you in touch.

arnaudauroux avatar Oct 11 '17 14:10 arnaudauroux

I also think this merits a redesign of IStartupConfigurationService as WebHostBuilder().ConfigureStartup<StartupConfigurationService>() can access IApplicationBuilder, IHostingEnvironment, IConfiguration, etc. directly. So no need for the below.

public Startup(IHostingEnvironment env, IStartupConfigurationService externalStartupConfiguration = null)
{
    this.externalStartupConfiguration = externalStartupConfiguration;
    this.externalStartupConfiguration.ConfigureEnvironment(env);

    // omitted
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    this.externalStartupConfiguration.Configure(app, env, loggerFactory, Configuration);

    // omitted
}

public void ConfigureServices(IServiceCollection services)
{
    // omitted
    
    this.externalStartupConfiguration.ConfigureServices(services, Configuration);
}

.ConfigureStartup<StartupConfigurationService>() should be all that is needed.

See ConfigureServices and ConfigureAppConfiguration for reference.

snebjorn avatar Oct 11 '17 15:10 snebjorn