Skip to content

Commit c80f3e5

Browse files
authored
Replace WebHostBuilder with HostBuilder pattern in MVC folder (#62703)
1 parent b2101a8 commit c80f3e5

File tree

32 files changed

+479
-206
lines changed

32 files changed

+479
-206
lines changed

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.Hosting;
2020

2121
internal sealed class GenericWebHostBuilder : WebHostBuilderBase, ISupportsStartup
2222
{
23-
private object? _startupObject;
23+
private const string _startupConfigName = "__UseStartup.StartupObject";
2424
private readonly object _startupKey = new object();
2525

2626
private AggregateException? _hostingStartupErrors;
@@ -170,13 +170,15 @@ public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptio
170170
UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
171171

172172
// UseStartup can be called multiple times. Only run the last one.
173-
_startupObject = startupType;
173+
_builder.Properties[_startupConfigName] = startupType;
174174

175175
_builder.ConfigureServices((context, services) =>
176176
{
177177
// Run this delegate if the startup type matches
178-
if (object.ReferenceEquals(_startupObject, startupType))
178+
if (_builder.Properties.TryGetValue(_startupConfigName, out var startupObject) &&
179+
object.ReferenceEquals(startupObject, startupType))
179180
{
181+
_builder.Properties.Remove(_startupConfigName);
180182
UseStartup(startupType, context, services);
181183
}
182184
});
@@ -193,16 +195,18 @@ public IWebHostBuilder UseStartup([DynamicallyAccessedMembers(StartupLinkerOptio
193195
UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
194196

195197
// Clear the startup type
196-
_startupObject = startupFactory;
198+
_builder.Properties[_startupConfigName] = startupFactory;
197199

198200
_builder.ConfigureServices(ConfigureStartup);
199201

200202
[UnconditionalSuppressMessage("Trimmer", "IL2072", Justification = "Startup type created by factory can't be determined statically.")]
201203
void ConfigureStartup(HostBuilderContext context, IServiceCollection services)
202204
{
203205
// UseStartup can be called multiple times. Only run the last one.
204-
if (object.ReferenceEquals(_startupObject, startupFactory))
206+
if (_builder.Properties.TryGetValue(_startupConfigName, out var startupObject) &&
207+
object.ReferenceEquals(startupObject, startupFactory))
205208
{
209+
_builder.Properties.Remove(_startupConfigName);
206210
var webHostBuilderContext = GetWebHostBuilderContext(context);
207211
var instance = startupFactory(webHostBuilderContext) ?? throw new InvalidOperationException("The specified factory returned null startup instance.");
208212
UseStartup(instance.GetType(), context, services, instance);
@@ -316,12 +320,14 @@ public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
316320
UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
317321

318322
// Clear the startup type
319-
_startupObject = configure;
323+
_builder.Properties[_startupConfigName] = configure;
320324

321325
_builder.ConfigureServices((context, services) =>
322326
{
323-
if (object.ReferenceEquals(_startupObject, configure))
327+
if (_builder.Properties.TryGetValue(_startupConfigName, out var startupObject) &&
328+
object.ReferenceEquals(startupObject, configure))
324329
{
330+
_builder.Properties.Remove(_startupConfigName);
325331
services.Configure<GenericWebHostServiceOptions>(options =>
326332
{
327333
options.ConfigureApplication = configure;
@@ -339,12 +345,14 @@ public IWebHostBuilder Configure(Action<WebHostBuilderContext, IApplicationBuild
339345
UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName);
340346

341347
// Clear the startup type
342-
_startupObject = configure;
348+
_builder.Properties[_startupConfigName] = configure;
343349

344350
_builder.ConfigureServices((context, services) =>
345351
{
346-
if (object.ReferenceEquals(_startupObject, configure))
352+
if (_builder.Properties.TryGetValue(_startupConfigName, out var startupObject) &&
353+
object.ReferenceEquals(startupObject, configure))
347354
{
355+
_builder.Properties.Remove(_startupConfigName);
348356
services.Configure<GenericWebHostServiceOptions>(options =>
349357
{
350358
var webhostBuilderContext = GetWebHostBuilderContext(context);

src/Hosting/Hosting/test/GenericWebHostBuilderTests.cs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Linq;
5+
using Microsoft.AspNetCore.Builder;
56
using Microsoft.AspNetCore.Hosting.Server;
67
using Microsoft.AspNetCore.Hosting.Server.Features;
8+
using Microsoft.AspNetCore.Http;
79
using Microsoft.AspNetCore.Http.Features;
810
using Microsoft.Extensions.Configuration;
911
using Microsoft.Extensions.DependencyInjection;
@@ -128,6 +130,86 @@ public void ReadsUrlsOrPorts(string urls, string httpPorts, string httpsPorts, s
128130
Assert.Equal(expected, string.Join(';', server.Addresses));
129131
}
130132

133+
[Fact]
134+
public async Task MultipleConfigureWebHostCallsWithUseStartupLastWins()
135+
{
136+
var server = new TestServer();
137+
138+
using var host = new HostBuilder()
139+
.ConfigureWebHost(webHostBuilder =>
140+
{
141+
webHostBuilder
142+
.UseServer(server)
143+
.UseStartup<FirstStartup>();
144+
})
145+
.ConfigureWebHost(webHostBuilder =>
146+
{
147+
webHostBuilder
148+
.UseStartup<SecondStartup>();
149+
})
150+
.Build();
151+
152+
await host.StartAsync();
153+
await AssertResponseContains(server.RequestDelegate, "SecondStartup");
154+
}
155+
156+
[Fact]
157+
public async Task MultipleConfigureWebHostCallsWithSameUseStartupOnlyRunsOne()
158+
{
159+
var server = new TestServer();
160+
161+
using var host = new HostBuilder()
162+
.ConfigureWebHost(webHostBuilder =>
163+
{
164+
webHostBuilder
165+
.UseServer(server)
166+
.UseStartup<FirstStartup>();
167+
})
168+
.ConfigureWebHost(webHostBuilder =>
169+
{
170+
webHostBuilder
171+
.UseStartup<FirstStartup>();
172+
})
173+
.Build();
174+
175+
await host.StartAsync();
176+
Assert.Single(host.Services.GetRequiredService<IEnumerable<FirstStartup>>());
177+
}
178+
179+
private async Task AssertResponseContains(RequestDelegate app, string expectedText)
180+
{
181+
var httpContext = new DefaultHttpContext();
182+
httpContext.Response.Body = new MemoryStream();
183+
await app(httpContext);
184+
httpContext.Response.Body.Seek(0, SeekOrigin.Begin);
185+
var bodyText = new StreamReader(httpContext.Response.Body).ReadToEnd();
186+
Assert.Contains(expectedText, bodyText);
187+
}
188+
189+
private class FirstStartup
190+
{
191+
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<FirstStartup>(); }
192+
193+
public void Configure(IApplicationBuilder app)
194+
{
195+
Assert.NotNull(app.ApplicationServices.GetService<FirstStartup>());
196+
Assert.Null(app.ApplicationServices.GetService<SecondStartup>());
197+
app.Run(context => context.Response.WriteAsync("FirstStartup"));
198+
}
199+
}
200+
201+
private class SecondStartup
202+
{
203+
public void ConfigureServices(IServiceCollection services) { services.AddSingleton<SecondStartup>(); }
204+
205+
public void Configure(IApplicationBuilder app)
206+
{
207+
Assert.Null(app.ApplicationServices.GetService<FirstStartup>());
208+
Assert.NotNull(app.ApplicationServices.GetService<SecondStartup>());
209+
app.Run(context => context.Response.WriteAsync("SecondStartup"));
210+
}
211+
}
212+
131213
private class TestServer : IServer, IServerAddressesFeature
132214
{
133215
public TestServer()
@@ -139,9 +221,31 @@ public TestServer()
139221

140222
public ICollection<string> Addresses { get; } = new List<string>();
141223
public bool PreferHostingUrls { get; set; }
224+
public RequestDelegate RequestDelegate { get; private set; }
142225

143-
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) => Task.CompletedTask;
144-
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
145226
public void Dispose() { }
227+
228+
public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken)
229+
{
230+
// For testing that uses RequestDelegate
231+
RequestDelegate = async ctx =>
232+
{
233+
var httpContext = application.CreateContext(ctx.Features);
234+
try
235+
{
236+
await application.ProcessRequestAsync(httpContext);
237+
}
238+
catch (Exception ex)
239+
{
240+
application.DisposeContext(httpContext, ex);
241+
throw;
242+
}
243+
application.DisposeContext(httpContext, null);
244+
};
245+
246+
return Task.CompletedTask;
247+
}
248+
249+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
146250
}
147251
}

src/Hosting/Hosting/test/WebHostBuilderTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,11 +1681,11 @@ public void Configure(IWebHostBuilder builder)
16811681
// This check is required because MVC still uses the
16821682
// IWebHostEnvironment instance before the container is baked
16831683
#pragma warning disable CS0618 // Type or member is obsolete
1684-
var heDescriptor = services.SingleOrDefault(s => s.ServiceType == typeof(IHostingEnvironment));
1684+
var heDescriptor = services.LastOrDefault(s => s.ServiceType == typeof(IHostingEnvironment));
16851685
Assert.NotNull(heDescriptor);
16861686
Assert.NotNull(heDescriptor.ImplementationInstance);
16871687
#pragma warning restore CS0618 // Type or member is obsolete
1688-
var wheDescriptor = services.SingleOrDefault(s => s.ServiceType == typeof(IWebHostEnvironment));
1688+
var wheDescriptor = services.LastOrDefault(s => s.ServiceType == typeof(IWebHostEnvironment));
16891689
Assert.NotNull(wheDescriptor);
16901690
Assert.NotNull(wheDescriptor.ImplementationInstance);
16911691
})

src/Mvc/Mvc.Testing/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.StartServer(
33
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.UseKestrel() -> void
44
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.UseKestrel(int port) -> void
55
Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.UseKestrel(System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerOptions!>! configureKestrelOptions) -> void
6+
virtual Microsoft.AspNetCore.Mvc.Testing.WebApplicationFactory<TEntryPoint>.CreateServer(System.IServiceProvider! serviceProvider) -> Microsoft.AspNetCore.TestHost.TestServer!

src/Mvc/Mvc.Testing/src/WebApplicationFactory.cs

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ internal virtual WebApplicationFactory<TEntryPoint> WithWebHostBuilderCore(Actio
143143
var factory = new DelegatedWebApplicationFactory(
144144
ClientOptions,
145145
CreateServer,
146+
CreateServer,
146147
CreateHost,
147148
CreateWebHostBuilder,
148149
CreateHostBuilder,
@@ -337,7 +338,10 @@ private void ConfigureHostBuilder(IHostBuilder hostBuilder)
337338
}
338339
else
339340
{
340-
webHostBuilder.UseTestServer();
341+
webHostBuilder.ConfigureServices(services =>
342+
{
343+
services.AddSingleton<IServer>(CreateServer);
344+
});
341345
}
342346
});
343347
_host = CreateHost(hostBuilder);
@@ -565,10 +569,19 @@ private static void EnsureDepsFile()
565569
/// <returns>The <see cref="TestServer"/> with the bootstrapped application.</returns>
566570
protected virtual TestServer CreateServer(IWebHostBuilder builder) => new(builder);
567571

572+
/// <summary>
573+
/// Creates the <see cref="TestServer"/> with the <see cref="IServiceProvider"/> from the bootstrapped application.
574+
/// This is only called for applications using <see cref="IHostBuilder"/>. Applications based on
575+
/// <see cref="IWebHostBuilder"/> will use <see cref="CreateHost"/> instead.
576+
/// </summary>
577+
/// <param name="serviceProvider">The <see cref="IServiceProvider"/> from the bootstrapped application.</param>
578+
/// <returns></returns>
579+
protected virtual TestServer CreateServer(IServiceProvider serviceProvider) => new(serviceProvider);
580+
568581
/// <summary>
569582
/// Creates the <see cref="IHost"/> with the bootstrapped application in <paramref name="builder"/>.
570583
/// This is only called for applications using <see cref="IHostBuilder"/>. Applications based on
571-
/// <see cref="IWebHostBuilder"/> will use <see cref="CreateServer"/> instead.
584+
/// <see cref="IWebHostBuilder"/> will use <see cref="CreateServer(IWebHostBuilder)"/> instead.
572585
/// </summary>
573586
/// <param name="builder">The <see cref="IHostBuilder"/> used to create the host.</param>
574587
/// <returns>The <see cref="IHost"/> with the bootstrapped application.</returns>
@@ -801,6 +814,7 @@ public virtual async ValueTask DisposeAsync()
801814
private sealed class DelegatedWebApplicationFactory : WebApplicationFactory<TEntryPoint>
802815
{
803816
private readonly Func<IWebHostBuilder, TestServer> _createServer;
817+
private readonly Func<IServiceProvider, TestServer> _createServerFromServiceProvider;
804818
private readonly Func<IHostBuilder, IHost> _createHost;
805819
private readonly Func<IWebHostBuilder?> _createWebHostBuilder;
806820
private readonly Func<IHostBuilder?> _createHostBuilder;
@@ -810,6 +824,7 @@ private sealed class DelegatedWebApplicationFactory : WebApplicationFactory<TEnt
810824
public DelegatedWebApplicationFactory(
811825
WebApplicationFactoryClientOptions options,
812826
Func<IWebHostBuilder, TestServer> createServer,
827+
Func<IServiceProvider, TestServer> createServerFromServiceProvider,
813828
Func<IHostBuilder, IHost> createHost,
814829
Func<IWebHostBuilder?> createWebHostBuilder,
815830
Func<IHostBuilder?> createHostBuilder,
@@ -819,6 +834,7 @@ public DelegatedWebApplicationFactory(
819834
{
820835
ClientOptions = new WebApplicationFactoryClientOptions(options);
821836
_createServer = createServer;
837+
_createServerFromServiceProvider = createServerFromServiceProvider;
822838
_createHost = createHost;
823839
_createWebHostBuilder = createWebHostBuilder;
824840
_createHostBuilder = createHostBuilder;
@@ -829,6 +845,8 @@ public DelegatedWebApplicationFactory(
829845

830846
protected override TestServer CreateServer(IWebHostBuilder builder) => _createServer(builder);
831847

848+
protected override TestServer CreateServer(IServiceProvider serviceProvider) => _createServerFromServiceProvider(serviceProvider);
849+
832850
protected override IHost CreateHost(IHostBuilder builder) => _createHost(builder);
833851

834852
protected override IWebHostBuilder? CreateWebHostBuilder() => _createWebHostBuilder();
@@ -846,6 +864,7 @@ internal override WebApplicationFactory<TEntryPoint> WithWebHostBuilderCore(Acti
846864
return new DelegatedWebApplicationFactory(
847865
ClientOptions,
848866
_createServer,
867+
_createServerFromServiceProvider,
849868
_createHost,
850869
_createWebHostBuilder,
851870
_createHostBuilder,

src/Mvc/perf/benchmarkapps/BasicApi/Startup.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.AspNetCore.Authentication.JwtBearer;
1212
using Microsoft.AspNetCore.Builder;
1313
using Microsoft.AspNetCore.Hosting;
14+
using Microsoft.Extensions.Hosting;
1415
using Microsoft.EntityFrameworkCore;
1516
using Microsoft.EntityFrameworkCore.Infrastructure;
1617
using Microsoft.EntityFrameworkCore.Migrations;
@@ -229,25 +230,29 @@ private void DropDatabaseTables(IServiceProvider services)
229230

230231
public static void Main(string[] args)
231232
{
232-
var host = CreateWebHostBuilder(args)
233+
using var host = CreateHost(args)
233234
.Build();
234235

235236
host.Run();
236237
}
237238

238-
public static IWebHostBuilder CreateWebHostBuilder(string[] args)
239+
public static IHostBuilder CreateHost(string[] args)
239240
{
240241
var configuration = new ConfigurationBuilder()
241242
.AddEnvironmentVariables()
242243
.AddCommandLine(args)
243244
.Build();
244245

245-
return new WebHostBuilder()
246-
.UseKestrel()
247-
.UseUrls("http://+:5000")
248-
.UseConfiguration(configuration)
249-
.UseContentRoot(Directory.GetCurrentDirectory())
250-
.UseStartup<Startup>();
246+
return new HostBuilder()
247+
.ConfigureWebHost(webHostBuilder =>
248+
{
249+
webHostBuilder
250+
.UseKestrel()
251+
.UseUrls("http://+:5000")
252+
.UseConfiguration(configuration)
253+
.UseContentRoot(Directory.GetCurrentDirectory())
254+
.UseStartup<Startup>();
255+
});
251256
}
252257
}
253258
}

0 commit comments

Comments
 (0)