Simple bootstrapping for ASP.NET Core applications running Autofac
install-package AutofacBoot
Placing all of your application bootstrapping tasks into a single Startup
file violates SRP. AutofacBoot allows you to define each bootstrapping task separately, keeping bootstrapping easier to write and maintain.
In an ASP.NET Core 2.0 application, your Program.cs
and Startup.cs
may look like this:
// Program.cs
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IHostingEnvironment environment)
{
var builder = new ConfigurationBuilder()
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
public void ConfigureContainer(ContainerBuilder builder)
{
// Autofac container registrations...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(this.Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMvc();
}
}
With AutofacBoot, you don't define a Startup
type, and your Program.cs
becomes:
public class Program
{
public static void Main(string[] args)
{
new AutofacBootstrapper().Run();
}
}
Your bootstrapping configuration then becomes separate tasks, for example:
Configuration:
public class ConfigurationBootstrapTask : IConfigurationBootstrapTask
{
public Task Execute(ConfigurationBuilder configurationBuilder, IHostingEnvironment environment)
{
configurationBuilder
.SetBasePath(environment.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
return Task.CompletedTask;
}
}
Services:
public class ServiceBootstrapTask : IServiceBootstrapTask
{
public Task Execute(IServiceCollection services)
{
services.AddMvc().AddApplicationPart(
typeof(MyController).Assembly);
return Task.CompletedTask;
}
}
Note if you are using the MVC service, you need to specify where your controllers are located by using
AddApplicationPart
and passing the assembly where your controllers reside.
Autofac container configuration:
public class ContainerBootstrapTask : IContainerBootstrapTask
{
public Task Execute(ContainerBuilder builder)
{
// access builder.RegisterType etc here...
return Task.CompletedTask;
}
}
Application configuration:
public class ApplicationBootstrapTask : IApplicationBootstrapTask
{
private readonly ILoggerFactory loggerFactory;
private readonly IConfigurationRoot configuration;
public ApplicationBootstrapTask(
ILoggerFactory loggerFactory,
IConfigurationRoot configuration)
{
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}
public Task Execute(IApplicationBuilder app)
{
this.loggerFactory.AddConsole(this.configuration.GetSection("Logging"));
this.loggerFactory.AddDebug();
app.UseMvc();
return Task.CompletedTask;
}
}
Note that any
IApplicationBootstrapTask
can have services injected via the constructor that have previously been registered in a service or Autofac container bootstrap task.
Tasks are run in the following order:
Type | Interface | Method | Can Inject Services |
---|---|---|---|
Configuration | IConfigurationBootstrapTask |
Task Execute(IHostingEnvironment environment, IConfigurationBuilder configurationBuilder) |
No |
Services | IServiceBootstrapTask |
Task Execute(IHostingEnvironment environment, IConfigurationRoot configuration, IServiceCollection services) |
No |
Container | IContainerBootstrapTask |
Task Execute(IHostingEnvironment environment, IConfigurationRoot configuration, ContainerBuilder builder) |
No |
Application | IApplicationBootstrapTask |
Task Execute(IApplicationBuilder app) |
Yes |
Service, Container, and Application tasks can be conditionally executed by adding the IConditionalExecution
interface to the task type and implementing the Task<bool> CanExecute(IHostingEnvironment environment, IConfigurationRoot configurationRoot)
method:
public class ApplicationWontExecuteTask : IApplicationBootstrapTask, IConditionalExecution
{
public Task<bool> CanExecute(IHostingEnvironment environment, IConfigurationRoot configurationRoot)
{
// Can check configuration and determine if this task is executed...
return Task.FromResult(false);
}
public Task Execute(IApplicationBuilder app)
{
// This won't be executed
return Task.CompletedTask;
}
}
You can change the order in which tasks are executed by adding the IOrderedTask
interface to the task type and implementing the int Order { get; }
property. The lower the value, the earlier in the pipeline the task will be executed. By default, all tasks have an order value of 0.
public class ApplicationLastBootstrapTask : IApplicationBootstrapTask, IOrderedTask
{
public int Order => 100;
public Task Execute(IApplicationBuilder app)
{
// This will execute after all other tasks with an order < 100
return Task.CompletedTask;
}
}
You likely want your logging configuration to run early in the bootstrap pipeline, therefore you can use IOrderedTask
and apply a low value (here, -10):
public class LoggingBootstrapTask : IApplicationBootstrapTask, IOrderedTask
{
public int Order { get; } = -10;
public Task Execute(IApplicationBuilder app)
{
Log.Logger = ...
return Task.CompletedTask;
}
}
install-package AutofacBoot.Test
For integration testing, it's useful to use the same bootstrapping process but have the ability to override service registrations for stubs and mocks.
The AutofacBoot.Test
package provides a TestServerFactory<TServerFactory>
abstract class which you can use to write integration tests using the same bootstrapping process. In its simplest form, create a server factory type deriving from TestServerFactory<TServerFactory>
and implement the Task<ITaskResolver> GetTaskResolver
method:
public class MyServerFactory : TestServerFactory<MyServerFactory>
{
protected override Task<ITaskResolver> GetTaskResolver()
{
ITaskResolver taskResolver = new AssemblyTaskResolver(
typeof(MyBootstrapTask).Assembly);
return Task.FromResult(taskResolver);
}
}
In this instance, the task resolver used is a provided AssemblyTaskResolver
which takes an assembly or collection of assemblies and scans them for task types. To use the same bootstrapping process in your tests as in production, specify the assembly that contains your production bootstrapping tasks.
You can then use the server factory within your tests:
[Theory]
[AutoData]
public async Task Return200(MyServerFactory serverFactory)
{
using (var server = await serverFactory.Create())
{
using (var client = server.CreateClient())
{
var response = await client.GetAsync("api/foo");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}
Note that AutoFixture is used here to allow the server factory to be injected into the test method, thus reducing any 'arrange' part of the test.
The server factory Create
method returns a standard TestServer
type provided by the Microsoft.AspNetCore.TestHost
package.
To override service registrations in your test, you can use the builder methods provided by the TestServerFactory
. These include With<TInterface, TImplementation>
for type registrations, and With<TInterface>(object instance)
for instance registrations.
Note that the registrations made within the
TestServerFactory
are executed last in the pipeline, overwriting any registrations made in the production bootstrapping.
For example:
[Theory]
[AutoData]
public async Task ValuesReturnsExpectedValues(
MyServerFactory serverFactory,
Mock<IValuesRepository> valuesRepository,
List<int> values)
{
valuesRepository.Setup(r => r.GetValues()).ReturnsAsync(values);
using (var server = await serverFactory
.With<IValuesRepository>(valuesRepository.Object)
.Create())
{
using (var client = server.CreateClient())
{
var response = await client.GetAsync("api/values");
var responseValues = await response.FromJsonCollection<int>();
Assert.Equal(values, responseValues);
}
}
}
Note that in this example Moq is used to test that the data returned from the repository is returned by the controller action.
AutofacBoot.Test
providesHttpResponseMessage
extensionsFromJson<T>
andFromJsonCollection<T>
for JSON deserialization.response.FromJsonCollection<int>()
is equivalent toresponse.FromJson<IEnumerable<int>>()
.
If you have many tests that use the same service setup, the test server factory supports configuring registrations within a separate type:
public class MyConfiguration : IServerFactoryConfiguration<MyServerFactory>
{
public void Configure(MyServerFactory factory)
{
factory.With<IValuesRepository>(...);
// etc.
}
}
To use the configuration, use the WithConfiguration
method:
using (var server = await serverFactory
.WithConfiguration(new MyConfiguration())
.Create())
{
...
}
You can use the WithConfigurations
method to register multiple configurations:
using (var server = await serverFactory
.WithConfigurations(new MyConfiguration(), new MyOtherConfiguration())
.Create())
{
...
}
The With
methods and configurations provide a mechanism for simple instance or type registrations, which are suitable in most use cases.
However, if you wish to have access to the full Autofac registration functionality within tests, you can use the WithContainerConfiguration
method.
This takes an IContainerConfiguration
which gives access to the Autofac ContainerBuilder
and the current environment:
public interface IContainerConfiguration
{
Task Configure(
IHostingEnvironment environment,
ContainerBuilder builder);
}
You can implement this interface to perform any Autofac registrations you wish:
public class MyContainerConfiguration : IContainerConfiguration
{
public Task Configure(
IHostingEnvironment environment,
ContainerBuilder builder)
{
// Perform any Autofac registrations
builder.RegisterGeneric(...)
return Task.CompletedTask;
}
}
Then use your container configuration within the server factory builder:
using (var server = await serverFactory
.WithContainerConfiguration(new MyContainerConfiguration())
.Create())
{
...
}
There are occasions when you may need to run custom middleware within your tests.
First, implement IAppBuilderConfiguration
which gives access to the ASP.NET Core IApplicationBuilder
:
public class MyAppBuilderConfiguration : IAppBuilderConfiguration
{
public Task Configure(IApplicationBuilder app)
{
// Add middleware here
return Task.CompletedTask;
}
}
Then use your app builder configuration within the test server factory using the WithAppBuilderConfiguration
method:
using (var server = await serverFactory
.WithAppBuilderConfiguration("id", new MyAppBuilderConfiguration())
.Create())
{
...
}
The WithAppBuilderConfiguration
also takes an identifier string which is used within your bootstrapping code to get access to the middleware. This gives flexibility to where your middleware is invoked in the pipeline.
To add your middleware, you can inject the provided IAppBuilderConfigurationResolver
provided by the AutofacBoot
package:
public class ApplicationBootstrapTask : IApplicationBootstrapTask
{
private readonly IAppBuilderConfigurationResolver configurationResolver;
public ApplicationBootstrapTask(IAppBuilderConfigurationResolver configurationResolver)
{
this.configurationResolver = configurationResolver
?? throw new ArgumentNullException(nameof(configurationResolver));
}
public Task Execute(IApplicationBuilder app)
{
var myMiddleware = this.configurationResolver.Resolve("id");
myMiddleware.Configure(app);
app.UseMvc();
return Task.CompletedTask;
}
}
The IAppBuilderConfigurationResolver
provides a Resolve
method which takes the same identifier string as registrered with the test server factory.
If the identifier doesn't exist (as would be the case when running the same bootstrapper in production), the Resolve
method returns a NullAppBuilderConfiguration
which does nothing, so you don't need to handle app builder configurations that are not registered.
If you do not wish to use string identifiers, you can also use the generic WithAppBuilderConfiguration
and Resolve
methods, which use the provided type's full name as the identifier:
using (var server = await serverFactory
.WithAppBuilderConfiguration<MyAppBuilderConfiguration>(
new MyAppBuilderConfiguration())
.Create())
{
...
}
Note this can be simplified to
WithAppBuilderConfiguration(new MyAppBuilderConfiguration())
Then, use the generic Resolve
method within your production bootstrap code:
var myMiddleware = this.configurationResolver.Resolve<MyAppBuilderConfiguration>();
myMiddleware.Configure(app);