Can I exclude code from WebApplicationFactory using the ASP.NET 6+ WebApplicationBuilder?

Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory will bootstrap an asp.net application in memory for unit tests. It used to do this by discovering and invoking public static IHostBuilder CreateHostBuilder.

That CreateHostBuilder method was part of an the old pattern from ASP.NET 3.x/5, which used a HostBuilder. Now we’ve got WebApplicationBuilder instead. And the test WebApplicationFactory will call Program.Main.

My problem is that Program.Main does stuff I don’t want done in unit tests.

The thing I don’t want in unit tests:

It sets up Seriolog from appsettings.json and appsettings.ASPNETCORE_ENVIRONMENT.json, and I don’t want that in my unit tests, it doesn’t make sense. It’s not something I can control WebApplicationFactory because I do it before I use WebApplication.CreateBuilder (which I think that’s a sensible decision: after entering Main my first priority is to configure logging… just in case)).

Is there anyway to convince WebApplicationFactory to choose another entry point that allows me to exclude my most basic infrastructual code from the unit test?

Do I have to go back to the old pattern of Program/Startup? Am I doing something misguided and should reconsider my need to exclude code from my tests?


In case it matters, here is my Program. Everything in the try would have previously been done via CreateHostBuilder(args).Build().Run() and been in Startup:

public class Program
{
    public static int Main(string[] args)
    {
        ConfigureLogging(configuration);

        try
        {
            var builder = WebApplication.CreateBuilder(args);
            _ = builder.Host.UseSerilog();
            _ = builder.Services.AddControllers().AddJsonOptions ...
            ...

            var app = builder.Build();
            ...

            Dependencies.Register(container, app.Configuration);
            container.Verify();
            
            app.Run();  
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }

        return 0;
    }

    private static void ConfigureLogging(IConfigurationRoot configuration)
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
            .AddEnvironmentVariables()
            .Build();
        Log.Logger = new LoggerConfiguration()
                .ReadFrom.Configuration(configuration)
                .Enrich.FromLogContext()
                ...
    }
}

and here is my test WebApplicationFactory

internal class MyWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly Dictionary<string, string?> _appConfig;
    public IConfiguration? Configuration { get; private set; }

    public MyWebApplicationFactory(SqlSettings sqlSettings)
    {
        _appConfig = new Dictionary<string, string?>
        {
            { "SqlSettings:ConnectionString", sqlSettings.ConnectionString },
        };
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        _ = builder.ConfigureAppConfiguration(config =>
        {
            // Clear config and replace with in memory
            // Won't affect anything that doesn't get config from app, like logging

            config.Sources.Clear();

            Configuration = new ConfigurationBuilder()
                .AddInMemoryCollection(_appConfig)
                .Build();
            _ = config.AddConfiguration(Configuration);
        });

        _ = builder.UseEnvironment("Test");
    }
}

As you can see. I have the opportunity to dump the configuration and add my own for the test. This will flow into my Dependencies.Register, but it is too late for logging.

And here’s what the old pattern was like. You can see that the basic infrastructural code would not be called by something trying to find CreateHostBuilder:

public class Program
{
    public static int Main(string[] args)
    {
        ConfigureLogging();
        try
        {
            CreateHostBuilder(args).Build().Run();
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }

        return 0;
    }

    private static void ConfigureLogging()
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            ...

        Log.Logger = new LoggerConfiguration()
            .ReadFrom.Configuration(configuration)
            ...
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(web =>
        {
            web.UseStartup<Startup>().UseSerilog();
        });
}

>Solution :

Personally I see 3 following options:

  1. Add some environment variable that will be checked by your ConfigureLogging method and disable it.

  2. Move away from manually building the configuration to reusing the one provided by the builder itself – builder.Configuration:

    var builder = WebApplication.CreateBuilder(args);
    ConfigureLogging(builder.Configuration);
    // ...
    
    static void ConfigureLogging(IConfigurationRoot configuration)
    {
        Log.Logger = new LoggerConfiguration()
             .ReadFrom.Configuration(configuration)
             .Enrich.FromLogContext()
             ...
    }
    

    WebApplication.CreateBuilder(args) throwing should be something very exceptional.

  3. The best one in my opinion for this use case – just switch back to the generic hosting and use the previous pattern.

Leave a Reply