1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- using System . Diagnostics . CodeAnalysis ;
5
4
using System . Linq ;
6
5
using System . Net . Http ;
7
6
using System . Reflection ;
8
7
using System . Text . Json ;
9
8
using System . Text . Json . Serialization ;
10
9
using Microsoft . AspNetCore . Hosting ;
11
10
using Microsoft . AspNetCore . Hosting . Server ;
11
+ using Microsoft . AspNetCore . Hosting . Server . Features ;
12
+ using Microsoft . AspNetCore . Server . Kestrel . Core ;
12
13
using Microsoft . AspNetCore . TestHost ;
13
14
using Microsoft . Extensions . Configuration ;
14
15
using Microsoft . Extensions . DependencyInjection ;
@@ -26,9 +27,16 @@ public partial class WebApplicationFactory<TEntryPoint> : IDisposable, IAsyncDis
26
27
{
27
28
private bool _disposed ;
28
29
private bool _disposedAsync ;
30
+
31
+ private bool _useKestrel ;
32
+ private int ? _kestrelPort ;
33
+ private Action < KestrelServerOptions > ? _configureKestrelOptions ;
34
+
29
35
private TestServer ? _server ;
30
36
private IHost ? _host ;
31
37
private Action < IWebHostBuilder > _configuration ;
38
+ private IWebHost ? _webHost ;
39
+ private Uri ? _webHostAddress ;
32
40
private readonly List < HttpClient > _clients = new ( ) ;
33
41
private readonly List < WebApplicationFactory < TEntryPoint > > _derivedFactories = new ( ) ;
34
42
@@ -75,8 +83,13 @@ public TestServer Server
75
83
{
76
84
get
77
85
{
78
- EnsureServer ( ) ;
79
- return _server ;
86
+ if ( _useKestrel )
87
+ {
88
+ throw new NotSupportedException ( Resources . TestServerNotSupportedWhenUsingKestrel ) ;
89
+ }
90
+
91
+ StartServer ( ) ;
92
+ return _server ! ;
80
93
}
81
94
}
82
95
@@ -87,11 +100,21 @@ public virtual IServiceProvider Services
87
100
{
88
101
get
89
102
{
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 ;
92
110
}
93
111
}
94
112
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
+
95
118
/// <summary>
96
119
/// Gets the <see cref="IReadOnlyList{WebApplicationFactory}"/> of factories created from this factory
97
120
/// by further customizing the <see cref="IWebHostBuilder"/> when calling
@@ -136,10 +159,92 @@ internal virtual WebApplicationFactory<TEntryPoint> WithWebHostBuilderCore(Actio
136
159
return factory ;
137
160
}
138
161
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 ( )
141
166
{
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 )
143
248
{
144
249
return ;
145
250
}
@@ -197,21 +302,53 @@ private void EnsureServer()
197
302
{
198
303
SetContentRoot ( builder ) ;
199
304
_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 ;
201
325
}
202
326
}
203
327
204
- [ MemberNotNull ( nameof ( _server ) ) ]
205
328
private void ConfigureHostBuilder ( IHostBuilder hostBuilder )
206
329
{
207
330
hostBuilder . ConfigureWebHost ( webHostBuilder =>
208
331
{
209
332
SetContentRoot ( webHostBuilder ) ;
210
333
_configuration ( webHostBuilder ) ;
211
- webHostBuilder . UseTestServer ( ) ;
334
+ if ( _useKestrel )
335
+ {
336
+ ConfigureBuilderToUseKestrel ( webHostBuilder ) ;
337
+ }
338
+ else
339
+ {
340
+ webHostBuilder . UseTestServer ( ) ;
341
+ }
212
342
} ) ;
213
343
_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
+ }
215
352
}
216
353
217
354
private void SetContentRoot ( IWebHostBuilder builder )
@@ -438,10 +575,15 @@ private static void EnsureDepsFile()
438
575
protected virtual IHost CreateHost ( IHostBuilder builder )
439
576
{
440
577
var host = builder . Build ( ) ;
578
+ TryConfigureServerPort ( ( ) => GetServerAddressFeature ( host ) ) ;
441
579
host . Start ( ) ;
442
580
return host ;
443
581
}
444
582
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
+
445
587
/// <summary>
446
588
/// Gives a fixture an opportunity to configure the application before it gets built.
447
589
/// </summary>
@@ -455,8 +597,21 @@ protected virtual void ConfigureWebHost(IWebHostBuilder builder)
455
597
/// redirects and handles cookies.
456
598
/// </summary>
457
599
/// <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
+ }
460
615
461
616
/// <summary>
462
617
/// Creates an instance of <see cref="HttpClient"/> that automatically follows
@@ -476,12 +631,24 @@ public HttpClient CreateClient(WebApplicationFactoryClientOptions options) =>
476
631
/// <returns>The <see cref="HttpClient"/>.</returns>
477
632
public HttpClient CreateDefaultClient ( params DelegatingHandler [ ] handlers )
478
633
{
479
- EnsureServer ( ) ;
634
+ StartServer ( ) ;
480
635
481
636
HttpClient client ;
482
637
if ( handlers == null || handlers . Length == 0 )
483
638
{
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
+ }
485
652
}
486
653
else
487
654
{
@@ -490,7 +657,7 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
490
657
handlers [ i - 1 ] . InnerHandler = handlers [ i ] ;
491
658
}
492
659
493
- var serverHandler = _server . CreateHandler ( ) ;
660
+ var serverHandler = CreateHandler ( ) ;
494
661
handlers [ ^ 1 ] . InnerHandler = serverHandler ;
495
662
496
663
client = new HttpClient ( handlers [ 0 ] ) ;
@@ -503,6 +670,21 @@ public HttpClient CreateDefaultClient(params DelegatingHandler[] handlers)
503
670
return client ;
504
671
}
505
672
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
+
506
688
/// <summary>
507
689
/// Configures <see cref="HttpClient"/> instances created by this <see cref="WebApplicationFactory{TEntryPoint}"/>.
508
690
/// </summary>
@@ -511,7 +693,19 @@ protected virtual void ConfigureClient(HttpClient client)
511
693
{
512
694
ArgumentNullException . ThrowIfNull ( client ) ;
513
695
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
+ }
515
709
}
516
710
517
711
/// <summary>
0 commit comments