Skip to content

Commit 113805a

Browse files
davidfowlTratcher
andauthored
Add support for instantiating the startup class (#24144)
* 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. * Update src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs Co-authored-by: Chris Ross <[email protected]> * PR feedback and bug fixes - Use actual throw expressions... - Added null checks Co-authored-by: Chris Ross <[email protected]>
1 parent 5eaad4c commit 113805a

7 files changed

+239
-17
lines changed

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

+36-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,31 @@ 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+
// UseStartup can be called multiple times. Only run the last one.
224+
if (object.ReferenceEquals(_startupObject, startupFactory))
225+
{
226+
var webHostBuilderContext = GetWebHostBuilderContext(context);
227+
var instance = startupFactory(webHostBuilderContext) ?? throw new InvalidOperationException("The specified factory returned null startup instance.");
228+
UseStartup(instance.GetType(), context, services, instance);
229+
}
230+
});
231+
232+
return this;
233+
}
234+
235+
private void UseStartup(Type startupType, HostBuilderContext context, IServiceCollection services, object instance = null)
214236
{
215237
var webHostBuilderContext = GetWebHostBuilderContext(context);
216238
var webHostOptions = (WebHostOptions)context.Properties[typeof(WebHostOptions)];
217239

218240
ExceptionDispatchInfo startupError = null;
219-
object instance = null;
220241
ConfigureBuilder configureBuilder = null;
221242

222243
try
@@ -231,7 +252,7 @@ private void UseStartup(Type startupType, HostBuilderContext context, IServiceCo
231252
throw new NotSupportedException($"ConfigureServices returning an {typeof(IServiceProvider)} isn't supported.");
232253
}
233254

234-
instance = ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
255+
instance ??= ActivatorUtilities.CreateInstance(new HostServiceProvider(webHostBuilderContext), startupType);
235256
context.Properties[_startupKey] = instance;
236257

237258
// Startup.ConfigureServices
@@ -296,13 +317,19 @@ private void ConfigureContainer<TContainer>(HostBuilderContext context, TContain
296317

297318
public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuilder> configure)
298319
{
320+
// Clear the startup type
321+
_startupObject = configure;
322+
299323
_builder.ConfigureServices((context, services) =>
300324
{
301-
services.Configure<GenericWebHostServiceOptions>(options =>
325+
if (object.ReferenceEquals(_startupObject, configure))
302326
{
303-
var webhostBuilderContext = GetWebHostBuilderContext(context);
304-
options.ConfigureApplication = app => configure(webhostBuilderContext, app);
305-
});
327+
services.Configure<GenericWebHostServiceOptions>(options =>
328+
{
329+
var webhostBuilderContext = GetWebHostBuilderContext(context);
330+
options.ConfigureApplication = app => configure(webhostBuilderContext, app);
331+
});
332+
}
306333
});
307334

308335
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

+47
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,48 @@ 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+
if (startupFactory == null)
73+
{
74+
throw new ArgumentNullException(nameof(startupFactory));
75+
}
76+
77+
var startupAssemblyName = startupFactory.GetMethodInfo().DeclaringType.GetTypeInfo().Assembly.GetName().Name;
78+
79+
hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
80+
81+
// Light up the GenericWebHostBuilder implementation
82+
if (hostBuilder is ISupportsStartup supportsStartup)
83+
{
84+
return supportsStartup.UseStartup(startupFactory);
85+
}
86+
87+
return hostBuilder
88+
.ConfigureServices((context, services) =>
89+
{
90+
services.AddSingleton(typeof(IStartup), sp =>
91+
{
92+
var instance = startupFactory(context) ?? throw new InvalidOperationException("The specified factory returned null startup instance.");
93+
94+
var hostingEnvironment = sp.GetRequiredService<IHostEnvironment>();
95+
96+
// Check if the instance implements IStartup before wrapping
97+
if (instance is IStartup startup)
98+
{
99+
return startup;
100+
}
101+
102+
return new ConventionBasedStartup(StartupLoader.LoadMethods(sp, instance.GetType(), hostingEnvironment.EnvironmentName, instance));
103+
});
104+
});
105+
}
64106

65107
/// <summary>
66108
/// Specify the startup type to be used by the web host.
@@ -70,6 +112,11 @@ private static IWebHostBuilder Configure(this IWebHostBuilder hostBuilder, Actio
70112
/// <returns>The <see cref="IWebHostBuilder"/>.</returns>
71113
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
72114
{
115+
if (startupType == null)
116+
{
117+
throw new ArgumentNullException(nameof(startupType));
118+
}
119+
73120
var startupAssemblyName = startupType.GetTypeInfo().Assembly.GetName().Name;
74121

75122
hostBuilder.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);

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
}

0 commit comments

Comments
 (0)