Skip to content

Commit 76846b0

Browse files
committed
Add support for instantiating the startup class
- Adds an overload of UseStartup that takes a factory so users can control the instance creation. The factory is given the WebHostBuilderContext to expose access to configuration and IWebHostEnvironment. - Make sure only one startup delegate runs, the last one registered.
1 parent d7bcde5 commit 76846b0

7 files changed

+168
-17
lines changed

src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs

+35-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISuppo
2121
{
2222
private readonly IHostBuilder _builder;
2323
private readonly IConfiguration _config;
24+
private object _startupObject;
2425
private readonly object _startupKey = new object();
2526

2627
private AggregateException _hostingStartupErrors;
@@ -198,10 +199,12 @@ public IWebHostBuilder UseDefaultServiceProvider(Action<WebHostBuilderContext, S
198199
public IWebHostBuilder UseStartup(Type startupType)
199200
{
200201
// UseStartup can be called multiple times. Only run the last one.
201-
_builder.Properties["UseStartup.StartupType"] = startupType;
202+
_startupObject = startupType;
203+
202204
_builder.ConfigureServices((context, services) =>
203205
{
204-
if (_builder.Properties.TryGetValue("UseStartup.StartupType", out var cachedType) && (Type)cachedType == startupType)
206+
// Run this delegate if the startup type matches
207+
if (object.ReferenceEquals(_startupObject, startupType))
205208
{
206209
UseStartup(startupType, context, services);
207210
}
@@ -210,13 +213,30 @@ public IWebHostBuilder UseStartup(Type startupType)
210213
return this;
211214
}
212215

213-
private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services)
216+
public IWebHostBuilder UseStartup(Func<WebHostBuilderContext, object> startupFactory)
217+
{
218+
// Clear the startup type
219+
_startupObject = startupFactory;
220+
221+
_builder.ConfigureServices((context, services) =>
222+
{
223+
if (object.ReferenceEquals(_startupObject, startupFactory))
224+
{
225+
var webHostBuilderContext = GetWebHostBuilderContext(context);
226+
var instance = startupFactory(webHostBuilderContext) ?? new InvalidOperationException("The specified factory returned null startup instance");
227+
UseStartup(instance.GetType(), context, services, instance);
228+
}
229+
});
230+
231+
return this;
232+
}
233+
234+
private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services, object instance = null)
214235
{
215236
var webHostBuilderContext = GetWebHostBuilderContext(context);
216237
var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
217238

218239
ExceptionDispatchInfo startupError = null;
219-
object instance = null;
220240
ConfigureBuilder configureBuilder = null;
221241

222242
try
@@ -231,7 +251,7 @@ private void UseStartup(Type startupType, HostBuilderContext context, IServiceCo
231251
throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported.");
232252
}
233253

234-
instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
254+
instance ??= ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
235255
context.Properties[_startupKey] = instance;
236256

237257
// Startup.ConfigureServices
@@ -296,13 +316,19 @@ private void ConfigureContainer<TContainer>(HostBuilderContext context, TContain
296316

297317
public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure)
298318
{
319+
// Clear the startup type
320+
_startupObject = configure;
321+
299322
_builder.ConfigureServices((context, services) =>
300323
{
301-
services.Configure<GenericWebHostServiceOptions>(options =>
324+
if (object.ReferenceEquals(_startupObject, configure))
302325
{
303-
var webhostBuilderContext = GetWebHostBuilderContext(context);
304-
options.ConfigureApplication = app => configure(webhostBuilderContext, app);
305-
});
326+
services.Configure<GenericWebHostServiceOptions>(options =>
327+
{
328+
var webhostBuilderContext = GetWebHostBuilderContext(context);
329+
options.ConfigureApplication = app => configure(webhostBuilderContext, app);
330+
});
331+
}
306332
});
307333

308334
return this;

src/Hosting/Hosting/src/GenericHost/HostingStartupWebHostBuilder.cs

+5
Original file line numberDiff line numberDiff line change
@@ -75,5 +75,10 @@ public IWebHostBuilder UseStartup(Type startupType)
7575
{
7676
return _builder.UseStartup(startupType);
7777
}
78+
79+
public IWebHostBuilder UseStartup(Func<WebHostBuilderContext, object> startupFactory)
80+
{
81+
return _builder.UseStartup(startupFactory);
82+
}
7883
}
7984
}

src/Hosting/Hosting/src/GenericHost/ISupportsStartup.cs

+1
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ internal interface ISupportsStartup
1010
{
1111
IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure);
1212
IWebHostBuilder UseStartup(Type startupType);
13+
IWebHostBuilder UseStartup(Func<WebHostBuilderContext, object> startupFactory);
1314
}
1415
}

src/Hosting/Hosting/src/Internal/StartupLoader.cs

+5-6
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,14 @@ internal class StartupLoader
3737
//
3838
// If the Startup class ConfigureServices returns an <see cref="IServiceProvider"/> and there is at least an <see cref="IStartupConfigureServicesFilter"/> registered we
3939
// throw as the filters can't be applied.
40-
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName)
40+
public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider, Type startupType, string environmentName, object instance = null)
4141
{
4242
var configureMethod = FindConfigureDelegate(startupType, environmentName);
4343

4444
var servicesMethod = FindConfigureServicesDelegate(startupType, environmentName);
4545
var configureContainerMethod = FindConfigureContainerDelegate(startupType, environmentName);
4646

47-
object instance = null;
48-
if (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic))
47+
if (instance == null && (!configureMethod.MethodInfo.IsStatic || (servicesMethod != null && !servicesMethod.MethodInfo.IsStatic)))
4948
{
5049
instance = ActivatorUtilities.GetServiceOrCreateInstance(hostingServiceProvider, startupType);
5150
}
@@ -54,7 +53,7 @@ public static StartupMethods LoadMethods(IServiceProvider hostingServiceProvider
5453
// going to be used for anything.
5554
var type = configureContainerMethod.MethodInfo != null ? configureContainerMethod.GetContainerType() : typeof(object);
5655

57-
var builder = (ConfigureServicesDelegateBuilder) Activator.CreateInstance(
56+
var builder = (ConfigureServicesDelegateBuilder)Activator.CreateInstance(
5857
typeof(ConfigureServicesDelegateBuilder<>).MakeGenericType(type),
5958
hostingServiceProvider,
6059
servicesMethod,
@@ -104,13 +103,13 @@ Action<object> ConfigureContainerPipeline(Action<object> action)
104103

105104
// The ConfigureContainer pipeline needs an Action<TContainerBuilder> as source, so we just adapt the
106105
// signature with this function.
107-
void Source(TContainerBuilder containerBuilder) =>
106+
void Source(TContainerBuilder containerBuilder) =>
108107
action(containerBuilder);
109108

110109
// The ConfigureContainerBuilder.ConfigureContainerFilters expects an Action<object> as value, but our pipeline
111110
// produces an Action<TContainerBuilder> given a source, so we wrap it on an Action<object> that internally casts
112111
// the object containerBuilder to TContainerBuilder to match the expected signature of our ConfigureContainer pipeline.
113-
void Target(object containerBuilder) =>
112+
void Target(object containerBuilder) =>
114113
BuildStartupConfigureContainerFiltersPipeline(Source)((TContainerBuilder)containerBuilder);
115114
}
116115
}

src/Hosting/Hosting/src/WebHostBuilderExtensions.cs

+31
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,37 @@ private static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Actio
6161
});
6262
}
6363

64+
/// <summary>
65+
/// Specify a factory that creates the startup instance to be used by the web host.
66+
/// </summary>
67+
/// <param name="hostBuilder">The <see cref="IWebHostBuilder"/> to configure.</param>
68+
/// <param name="startupFactory">A delegate that specifies a factory for the startup class.</param>
69+
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
70+
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Func<WebHostBuilderContext, object> startupFactory)
71+
{
72+
var startupAssemblyName = startupFactory.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name;
73+
74+
hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
75+
76+
// Light up the GenericWebHostBuilder implementation
77+
if (hostBuilder is ISupportsStartup supportsStartup)
78+
{
79+
return supportsStartup.UseStartup(startupFactory);
80+
}
81+
82+
return hostBuilder
83+
.ConfigureServices((context, services) =>
84+
{
85+
services.AddSingleton(typeof(IStartup), sp =>
86+
{
87+
var instance = startupFactory(context) ?? new InvalidOperationException("The specified factory returned null startup instance");
88+
89+
var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
90+
91+
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, instance.GetType(), hostingEnvironment.EnvironmentName, instance));
92+
});
93+
});
94+
}
6495

6596
/// <summary>
6697
/// Specify the startup type to be used by the web host.

src/Hosting/Hosting/test/Fakes/GenericWebHostBuilderWrapper.cs

+6
Original file line numberDiff line numberDiff line change
@@ -73,5 +73,11 @@ public IWebHostBuilder UseStartup(Type startupType)
7373
_builder.UseStartup(startupType);
7474
return this;
7575
}
76+
77+
public IWebHostBuilder UseStartup(Func<WebHostBuilderContext, object> startupFactory)
78+
{
79+
_builder.UseStartup(startupFactory);
80+
return this;
81+
}
7682
}
7783
}

src/Hosting/Hosting/test/WebHostBuilderTests.cs

+85-2
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,19 @@
66
using System.IO;
77
using System.Linq;
88
using System.Reflection;
9+
using System.Runtime.ExceptionServices;
910
using System.Threading;
1011
using System.Threading.Tasks;
12+
using System.Web;
1113
using Microsoft.AspNetCore.Builder;
1214
using Microsoft.AspNetCore.Hosting;
1315
using Microsoft.AspNetCore.Hosting.Fakes;
1416
using Microsoft.AspNetCore.Hosting.Server;
1517
using Microsoft.AspNetCore.Hosting.Tests.Fakes;
1618
using Microsoft.AspNetCore.Http;
19+
using Microsoft.AspNetCore.Http.Extensions;
1720
using Microsoft.AspNetCore.Http.Features;
21+
using Microsoft.AspNetCore.WebUtilities;
1822
using Microsoft.Extensions.Configuration;
1923
using Microsoft.Extensions.DependencyInjection;
2024
using Microsoft.Extensions.Hosting;
@@ -68,6 +72,54 @@ public async Task StartupStaticCtorThrows_Fallback(IWebHostBuilder builder)
6872
}
6973
}
7074

75+
[Theory]
76+
[MemberData(nameof(DefaultWebHostBuildersWithConfig))]
77+
public async Task MultipleUseStartupCallsLastWins(IWebHostBuilder builder)
78+
{
79+
var server = new TestServer();
80+
var host = builder.UseServer(server)
81+
.UseStartup<StartupCtorThrows>()
82+
.UseStartup(context => throw new InvalidOperationException("This doesn't run"))
83+
.Configure(app =>
84+
{
85+
throw new InvalidOperationException("This doesn't run");
86+
})
87+
.Configure(app =>
88+
{
89+
app.Run(context =>
90+
{
91+
return context.Response.WriteAsync("This wins");
92+
});
93+
})
94+
.Build();
95+
using (host)
96+
{
97+
await host.StartAsync();
98+
await AssertResponseContains(server.RequestDelegate, "This wins");
99+
}
100+
}
101+
102+
[Theory]
103+
[MemberData(nameof(DefaultWebHostBuildersWithConfig))]
104+
public async Task UseStartupFactoryWorks(IWebHostBuilder builder)
105+
{
106+
void ConfigureServices(IServiceCollection services) { }
107+
void Configure(IApplicationBuilder app)
108+
{
109+
app.Run(context => context.Response.WriteAsync("UseStartupFactoryWorks"));
110+
}
111+
112+
var server = new TestServer();
113+
var host = builder.UseServer(server)
114+
.UseStartup(context => new DelegatingStartup(ConfigureServices, Configure))
115+
.Build();
116+
using (host)
117+
{
118+
await host.StartAsync();
119+
await AssertResponseContains(server.RequestDelegate, "UseStartupFactoryWorks");
120+
}
121+
}
122+
71123
[Theory]
72124
[MemberData(nameof(DefaultWebHostBuildersWithConfig))]
73125
public async Task StartupCtorThrows_Fallback(IWebHostBuilder builder)
@@ -199,7 +251,7 @@ public void ConfigureDefaultServiceProviderWithContext(IWebHostBuilder builder)
199251
options.ValidateScopes = true;
200252
});
201253

202-
using var host = hostBuilder.Build();
254+
using var host = hostBuilder.Build();
203255
Assert.Throws<InvalidOperationException>(() => host.Start());
204256
Assert.True(configurationCallbackCalled);
205257
}
@@ -728,6 +780,22 @@ public void DefaultApplicationNameWithConfigure(IWebHostBuilder builder)
728780
}
729781
}
730782

783+
[Theory]
784+
[MemberData(nameof(DefaultWebHostBuilders))]
785+
public void DefaultApplicationNameWithUseStartupFactory(IWebHostBuilder builder)
786+
{
787+
using (var host = builder
788+
.UseServer(new TestServer())
789+
.UseStartup(context => new DelegatingStartup(s => { }, app => { }))
790+
.Build())
791+
{
792+
var hostingEnv = host.Services.GetService<IHostEnvironment>();
793+
794+
// Should be the assembly containing this test, because that's where the delegate comes from
795+
Assert.Equal(typeof(WebHostBuilderTests).Assembly.GetName().Name, hostingEnv.ApplicationName);
796+
}
797+
}
798+
731799
[Theory]
732800
[MemberData(nameof(DefaultWebHostBuilders))]
733801
public void Configure_SupportsNonStaticMethodDelegate(IWebHostBuilder builder)
@@ -1218,7 +1286,7 @@ public void UseConfigurationWithSectionAddsSubKeys(IWebHostBuilder builder)
12181286

12191287
Assert.Equal("nestedvalue", builder.GetSetting("key"));
12201288

1221-
using var host = builder.Build();
1289+
using var host = builder.Build();
12221290
var appConfig = host.Services.GetRequiredService<IConfiguration>();
12231291
Assert.Equal("nestedvalue", appConfig["key"]);
12241292
}
@@ -1574,6 +1642,21 @@ public void Configure(IWebHostBuilder builder)
15741642
}
15751643
}
15761644

1645+
public class DelegatingStartup
1646+
{
1647+
private readonly Action<IServiceCollection> _configureServices;
1648+
private readonly Action<IApplicationBuilder> _configure;
1649+
1650+
public DelegatingStartup(Action<IServiceCollection> configureServices, Action<IApplicationBuilder> configure)
1651+
{
1652+
_configureServices = configureServices;
1653+
_configure = configure;
1654+
}
1655+
1656+
public void ConfigureServices(IServiceCollection services) => _configureServices(services);
1657+
public void Configure(IApplicationBuilder app) => _configure(app);
1658+
}
1659+
15771660
public class StartupWithResolvedDisposableThatThrows
15781661
{
15791662
public StartupWithResolvedDisposableThatThrows(DisposableService service)

0 commit comments

Comments
 (0)