11// Licensed to the .NET Foundation under one or more agreements.
22// The .NET Foundation licenses this file to you under the MIT license.
33
4- using System . Diagnostics . CodeAnalysis ;
54using System . Linq ;
65using System . Net . Http ;
76using System . Reflection ;
87using System . Text . Json ;
98using System . Text . Json . Serialization ;
109using Microsoft . AspNetCore . Hosting ;
1110using Microsoft . AspNetCore . Hosting . Server ;
11+ using Microsoft . AspNetCore . Hosting . Server . Features ;
12+ using Microsoft . AspNetCore . Server . Kestrel . Core ;
1213using Microsoft . AspNetCore . TestHost ;
1314using Microsoft . Extensions . Configuration ;
1415using Microsoft . Extensions . DependencyInjection ;
@@ -26,9 +27,16 @@ public partial class WebApplicationFactory<TEntryPoint> : IDisposable, IAsyncDis
2627{
2728 private bool _disposed ;
2829 private bool _disposedAsync ;
30+
31+ private bool _useKestrel ;
32+ private int ? _kestrelPort ;
33+ private Action < KestrelServerOptions > ? _configureKestrelOptions ;
34+
2935 private TestServer ? _server ;
3036 private IHost ? _host ;
3137 private Action < IWebHostBuilder > _configuration ;
38+ private IWebHost ? _webHost ;
39+ private Uri ? _webHostAddress ;
3240 private readonly List < HttpClient > _clients = new ( ) ;
3341 private readonly List < WebApplicationFactory < TEntryPoint > > _derivedFactories = new ( ) ;
3442
@@ -75,8 +83,13 @@ public TestServer Server
7583 {
7684 get
7785 {
78- EnsureServer ( ) ;
79- return _server ;
86+ if ( _useKestrel )
87+ {
88+ throw new NotSupportedException ( Resources . TestServerNotSupportedWhenUsingKestrel ) ;
89+ }
90+
91+ StartServer ( ) ;
92+ return _server ! ;
8093 }
8194 }
8295
@@ -87,11 +100,21 @@ public virtual IServiceProvider Services
87100 {
88101 get
89102 {
90- EnsureServer ( ) ;
91- return _host ? . Services ?? _server . Host . Services ;
103+ StartServer ( ) ;
104+ if ( _useKestrel )
105+ {
106+ return _webHost ! . Services ;
107+ }
108+
109+ return _host ? . Services ?? _server ! . Host . Services ;
92110 }
93111 }
94112
113+ /// <summary>
114+ /// Helps determine if the `StartServer` method has been called already.
115+ /// </summary>
116+ private bool ServerStarted => _webHost != null || _host != null || _server != null ;
117+
95118 /// <summary>
96119 /// Gets the <see cref="IReadOnlyList{WebApplicationFactory}"/> of factories created from this factory
97120 /// by further customizing the <see cref="IWebHostBuilder"/> when calling
@@ -136,10 +159,92 @@ internal virtual WebApplicationFactory<TEntryPoint> WithWebHostBuilderCore(Actio
136159 return factory ;
137160 }
138161
139- [ MemberNotNull ( nameof ( _server ) ) ]
140- private void EnsureServer ( )
162+ /// <summary>
163+ /// Configures the factory to use Kestrel as the server.
164+ /// </summary>
165+ public void UseKestrel ( )
141166 {
142- if ( _server != null )
167+ if ( ServerStarted )
168+ {
169+ throw new InvalidOperationException ( Resources . UseKestrelCanBeCalledBeforeInitialization ) ;
170+ }
171+
172+ _useKestrel = true ;
173+ }
174+
175+ /// <summary>
176+ /// Configures the factory to use Kestrel as the server.
177+ /// </summary>
178+ /// <param name="port">The port to listen to when the server starts. Use `0` to allow dynamic port selection.</param>
179+ /// <exception cref="InvalidOperationException">Thrown, if this method is called after the WebHostFactory has been initialized.</exception>
180+ /// <remarks>This method should be called before the factory is initialized either via one of the <see cref="CreateClient()"/> methods
181+ /// or via the <see cref="StartServer"/> method.</remarks>
182+ public void UseKestrel ( int port )
183+ {
184+ UseKestrel ( ) ;
185+
186+ this . _kestrelPort = port ;
187+ }
188+
189+ /// <summary>
190+ /// Configures the factory to use Kestrel as the server.
191+ /// </summary>
192+ /// <param name="configureKestrelOptions">A callback handler that will be used for configuring the server when it starts.</param>
193+ /// <exception cref="InvalidOperationException">Thrown, if this method is called after the WebHostFactory has been initialized.</exception>
194+ /// <remarks>This method should be called before the factory is initialized either via one of the <see cref="CreateClient()"/> methods
195+ /// or via the <see cref="StartServer"/> method.</remarks>
196+ public void UseKestrel ( Action < KestrelServerOptions > configureKestrelOptions )
197+ {
198+ UseKestrel ( ) ;
199+
200+ this . _configureKestrelOptions = configureKestrelOptions ;
201+ }
202+
203+ private IWebHost CreateKestrelServer ( IWebHostBuilder builder )
204+ {
205+ ConfigureBuilderToUseKestrel ( builder ) ;
206+
207+ var host = builder . Build ( ) ;
208+
209+ TryConfigureServerPort ( ( ) => GetServerAddressFeature ( host ) ) ;
210+
211+ host . Start ( ) ;
212+ return host ;
213+ }
214+
215+ private void TryConfigureServerPort ( Func < IServerAddressesFeature ? > serverAddressFeatureAccessor )
216+ {
217+ if ( _kestrelPort . HasValue )
218+ {
219+ var saf = serverAddressFeatureAccessor ( ) ;
220+ if ( saf is not null )
221+ {
222+ saf . Addresses . Clear ( ) ;
223+ saf . Addresses . Add ( $ "http://127.0.0.1:{ _kestrelPort } ") ;
224+ saf . PreferHostingUrls = true ;
225+ }
226+ }
227+ }
228+
229+ private void ConfigureBuilderToUseKestrel ( IWebHostBuilder builder )
230+ {
231+ if ( _configureKestrelOptions is not null )
232+ {
233+ builder . UseKestrel ( _configureKestrelOptions ) ;
234+ }
235+ else
236+ {
237+ builder . UseKestrel ( ) ;
238+ }
239+ }
240+
241+ /// <summary>
242+ /// Initializes the instance by configurating the host builder.
243+ /// </summary>
244+ /// <exception cref="InvalidOperationException">Thrown if the provided <typeparamref name="TEntryPoint"/> type has no factory method.</exception>
245+ public void StartServer ( )
246+ {
247+ if ( ServerStarted )
143248 {
144249 return ;
145250 }
@@ -197,21 +302,53 @@ private void EnsureServer()
197302 {
198303 SetContentRoot ( builder ) ;
199304 _configuration ( builder ) ;
200- _server = CreateServer ( builder ) ;
305+ if ( _useKestrel )
306+ {
307+ _webHost = CreateKestrelServer ( builder ) ;
308+
309+ TryExtractHostAddress ( GetServerAddressFeature ( _webHost ) ) ;
310+ }
311+ else
312+ {
313+ _server = CreateServer ( builder ) ;
314+ }
315+ }
316+ }
317+
318+ private void TryExtractHostAddress ( IServerAddressesFeature ? serverAddressFeature )
319+ {
320+ if ( serverAddressFeature ? . Addresses . Count > 0 )
321+ {
322+ // Store the web host address as it's going to be used every time a client is created to communicate to the server
323+ _webHostAddress = new Uri ( serverAddressFeature . Addresses . Last ( ) ) ;
324+ ClientOptions . BaseAddress = _webHostAddress ;
201325 }
202326 }
203327
204- [ MemberNotNull ( nameof ( _server ) ) ]
205328 private void ConfigureHostBuilder ( IHostBuilder hostBuilder )
206329 {
207330 hostBuilder . ConfigureWebHost ( webHostBuilder =>
208331 {
209332 SetContentRoot ( webHostBuilder ) ;
210333 _configuration ( webHostBuilder ) ;
211- webHostBuilder . UseTestServer ( ) ;
334+ if ( _useKestrel )
335+ {
336+ ConfigureBuilderToUseKestrel ( webHostBuilder ) ;
337+ }
338+ else
339+ {
340+ webHostBuilder . UseTestServer ( ) ;
341+ }
212342 } ) ;
213343 _host = CreateHost ( hostBuilder ) ;
214- _server = ( TestServer ) _host . Services . GetRequiredService < IServer > ( ) ;
344+ if ( _useKestrel )
345+ {
346+ TryExtractHostAddress ( GetServerAddressFeature ( _host ) ) ;
347+ }
348+ else
349+ {
350+ _server = ( TestServer ) _host . Services . GetRequiredService < IServer > ( ) ;
351+ }
215352 }
216353
217354 private void SetContentRoot ( IWebHostBuilder builder )
@@ -438,10 +575,15 @@ private static void EnsureDepsFile()
438575 protected virtual IHost CreateHost ( IHostBuilder builder )
439576 {
440577 var host = builder . Build ( ) ;
578+ TryConfigureServerPort ( ( ) => GetServerAddressFeature ( host ) ) ;
441579 host . Start ( ) ;
442580 return host ;
443581 }
444582
583+ private static IServerAddressesFeature ? GetServerAddressFeature ( IHost host ) => host . Services . GetRequiredService < IServer > ( ) . Features . Get < IServerAddressesFeature > ( ) ;
584+
585+ private static IServerAddressesFeature ? GetServerAddressFeature ( IWebHost webHost ) => webHost . ServerFeatures . Get < IServerAddressesFeature > ( ) ;
586+
445587 /// <summary>
446588 /// Gives a fixture an opportunity to configure the application before it gets built.
447589 /// </summary>
@@ -455,8 +597,21 @@ protected virtual void ConfigureWebHost(IWebHostBuilder builder)
455597 /// redirects and handles cookies.
456598 /// </summary>
457599 /// <returns>The <see cref="HttpClient"/>.</returns>
458- public HttpClient CreateClient ( ) =>
459- CreateClient ( ClientOptions ) ;
600+ public HttpClient CreateClient ( )
601+ {
602+ var client = CreateClient ( ClientOptions ) ;
603+
604+ if ( _useKestrel && object . ReferenceEquals ( client . BaseAddress , WebApplicationFactoryClientOptions . DefaultBaseAddres ) )
605+ {
606+ // When using Kestrel, the server may start to listen on a pre-configured port,
607+ // which can differ from the one configured via ClientOptions.
608+ // Hence, if the ClientOptions haven't been set explicitly to a custom value, we will assume that
609+ // the user wants the client to communicate on a port that the server listens too, and because that port may be different, we overwrite it here.
610+ client . BaseAddress = ClientOptions . BaseAddress ;
611+ }
612+
613+ return client ;
614+ }
460615
461616 /// <summary>
462617 /// Creates an instance of <see cref="HttpClient"/> that automatically follows
@@ -476,12 +631,24 @@ public HttpClient CreateClient(WebApplicationFactoryClientOptions options) =>
476631 /// <returns>The <see cref="HttpClient"/>.</returns>
477632 public HttpClient CreateDefaultClient ( params DelegatingHandler [ ] handlers )
478633 {
479- EnsureServer ( ) ;
634+ StartServer ( ) ;
480635
481636 HttpClient client ;
482637 if ( handlers == null || handlers . Length == 0 )
483638 {
484- client = _server . CreateClient ( ) ;
639+ if ( _useKestrel )
640+ {
641+ client = new HttpClient ( ) ;
642+ }
643+ else
644+ {
645+ if ( _server is null )
646+ {
647+ throw new InvalidOperationException ( Resources . ServerNotInitialized ) ;
648+ }
649+
650+ client = _server . CreateClient ( ) ;
651+ }
485652 }
486653 else
487654 {
@@ -490,7 +657,7 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
490657 handlers [ i - 1 ] . InnerHandler = handlers [ i ] ;
491658 }
492659
493- var serverHandler = _server . CreateHandler ( ) ;
660+ var serverHandler = CreateHandler ( ) ;
494661 handlers [ ^ 1 ] . InnerHandler = serverHandler ;
495662
496663 client = new HttpClient ( handlers [ 0 ] ) ;
@@ -503,6 +670,21 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
503670 return client ;
504671 }
505672
673+ private HttpMessageHandler CreateHandler ( )
674+ {
675+ if ( _useKestrel )
676+ {
677+ return new HttpClientHandler ( ) ;
678+ }
679+
680+ if ( _server is null )
681+ {
682+ throw new InvalidOperationException ( Resources . ServerNotInitialized ) ;
683+ }
684+
685+ return _server . CreateHandler ( ) ;
686+ }
687+
506688 /// <summary>
507689 /// Configures <see cref="HttpClient"/> instances created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
508690 /// </summary>
@@ -511,7 +693,19 @@ protected virtual void ConfigureClient(HttpClient client)
511693 {
512694 ArgumentNullException . ThrowIfNull ( client ) ;
513695
514- client . BaseAddress = new Uri ( "http://localhost" ) ;
696+ if ( _useKestrel )
697+ {
698+ if ( _webHost is null && _host is null )
699+ {
700+ throw new InvalidOperationException ( Resources . ServerNotInitialized ) ;
701+ }
702+
703+ client . BaseAddress = _webHostAddress ;
704+ }
705+ else
706+ {
707+ client . BaseAddress = new Uri ( "http://localhost" ) ;
708+ }
515709 }
516710
517711 /// <summary>
0 commit comments