| File: TestServer.cs | Web Access |
| Project: src\aspnetcore\src\Hosting\TestHost\src\Microsoft.AspNetCore.TestHost.csproj (Microsoft.AspNetCore.TestHost) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Shared; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.TestHost; /// <summary> /// An <see cref="IServer"/> implementation for executing tests. /// </summary> public class TestServer : IServer { #pragma warning disable ASPDEPR008 // IWebHost is obsolete private readonly IWebHost? _hostInstance; #pragma warning restore ASPDEPR008 // IWebHost is obsolete private bool _disposed; private ApplicationWrapper? _application; private static FeatureCollection CreateTestFeatureCollection() { var features = new FeatureCollection(); features.Set<IServerAddressesFeature>(new ServerAddressesFeature()); return features; } /// <summary> /// For use with IHostBuilder. /// </summary> /// <param name="services"></param> /// <param name="optionsAccessor"></param> public TestServer(IServiceProvider services, IOptions<TestServerOptions> optionsAccessor) : this(services, CreateTestFeatureCollection(), optionsAccessor) { } /// <summary> /// For use with IHostBuilder. /// </summary> /// <param name="services"></param> /// <param name="featureCollection"></param> /// <param name="optionsAccessor"></param> public TestServer(IServiceProvider services, IFeatureCollection featureCollection, IOptions<TestServerOptions> optionsAccessor) { Services = services ?? throw new ArgumentNullException(nameof(services)); Features = featureCollection ?? throw new ArgumentNullException(nameof(featureCollection)); var options = optionsAccessor?.Value ?? throw new ArgumentNullException(nameof(optionsAccessor)); AllowSynchronousIO = options.AllowSynchronousIO; PreserveExecutionContext = options.PreserveExecutionContext; BaseAddress = options.BaseAddress; } /// <summary> /// For use with IHostBuilder. /// </summary> /// <param name="services"></param> public TestServer(IServiceProvider services) : this(services, CreateTestFeatureCollection()) { } /// <summary> /// For use with IHostBuilder. /// </summary> /// <param name="services"></param> /// <param name="featureCollection"></param> public TestServer(IServiceProvider services, IFeatureCollection featureCollection) : this(services, featureCollection, Options.Create(new TestServerOptions())) { Services = services ?? throw new ArgumentNullException(nameof(services)); Features = featureCollection ?? throw new ArgumentNullException(nameof(featureCollection)); } /// <summary> /// For use with IWebHostBuilder. /// </summary> /// <param name="builder"></param> [Obsolete("IWebHost, which this method uses, is obsolete. Use one of the ctors that takes an IServiceProvider instead. For more information, visit https://aka.ms/aspnet/deprecate/008.", DiagnosticId = "ASPDEPR008", UrlFormat = Obsoletions.AspNetCoreDeprecate008Url)] public TestServer(IWebHostBuilder builder) : this(builder, CreateTestFeatureCollection()) { } /// <summary> /// For use with IWebHostBuilder. /// </summary> /// <param name="builder"></param> /// <param name="featureCollection"></param> [Obsolete("IWebHost, which this method uses, is obsolete. Use one of the ctors that takes an IServiceProvider instead. For more information, visit https://aka.ms/aspnet/deprecate/008.", DiagnosticId = "ASPDEPR008", UrlFormat = Obsoletions.AspNetCoreDeprecate008Url)] public TestServer(IWebHostBuilder builder, IFeatureCollection featureCollection) { ArgumentNullException.ThrowIfNull(builder); Features = featureCollection ?? throw new ArgumentNullException(nameof(featureCollection)); var host = builder.UseServer(this).Build(); host.StartAsync().GetAwaiter().GetResult(); _hostInstance = host; Services = host.Services; } /// <summary> /// Gets or sets the base address associated with the HttpClient returned by the test server. Defaults to http://localhost/. /// </summary> public Uri BaseAddress { get; set; } = new Uri("http://localhost/"); /// <summary> /// Gets the <see cref="IWebHost" /> instance associated with the test server. /// </summary> [Obsolete("IWebHost is obsolete. Use IHost instead. For more information, visit https://aka.ms/aspnet/deprecate/008.", DiagnosticId = "ASPDEPR008", UrlFormat = Obsoletions.AspNetCoreDeprecate008Url)] public IWebHost Host { get { return _hostInstance ?? throw new InvalidOperationException("The TestServer constructor was not called with a IWebHostBuilder so IWebHost is not available."); } } /// <summary> /// Gets the service provider associated with the test server. /// </summary> public IServiceProvider Services { get; } /// <summary> /// Gets the collection of server features associated with the test server. /// </summary> public IFeatureCollection Features { get; } /// <summary> /// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/>. The default value is <see langword="false" />. /// </summary> public bool AllowSynchronousIO { get; set; } /// <summary> /// Gets or sets a value that controls if <see cref="ExecutionContext"/> and <see cref="AsyncLocal{T}"/> values are preserved from the client to the server. The default value is <see langword="false" />. /// </summary> public bool PreserveExecutionContext { get; set; } private ApplicationWrapper Application { get => _application ?? throw new InvalidOperationException("The server has not been started or no web application was configured."); } private PathString PathBase => BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); /// <summary> /// Creates a custom <see cref="HttpMessageHandler" /> for processing HTTP requests/responses with the test server. /// </summary> public HttpMessageHandler CreateHandler() { return new ClientHandler(PathBase, Application) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext }; } /// <summary> /// Creates a custom <see cref="HttpMessageHandler" /> for processing HTTP requests/responses with custom configuration with the test server. /// </summary> public HttpMessageHandler CreateHandler(Action<HttpContext> additionalContextConfiguration) { return new ClientHandler(PathBase, Application, additionalContextConfiguration) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext }; } /// <summary> /// Creates a <see cref="HttpClient" /> for processing HTTP requests/responses with the test server. /// </summary> public HttpClient CreateClient() { return new HttpClient(CreateHandler()) { BaseAddress = BaseAddress, Timeout = TimeSpan.FromSeconds(200), }; } /// <summary> /// Creates a <see cref="WebSocketClient" /> for interacting with the test server. /// </summary> public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext }; } /// <summary> /// Begins constructing a request message for submission. /// </summary> /// <param name="path"></param> /// <returns><see cref="RequestBuilder"/> to use in constructing additional request details.</returns> public RequestBuilder CreateRequest(string path) { return new RequestBuilder(this, path); } /// <summary> /// Creates, configures, sends, and returns a <see cref="HttpContext"/>. This completes as soon as the response is started. /// </summary> /// <returns></returns> public async Task<HttpContext> SendAsync(Action<HttpContext> configureContext, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(configureContext); var builder = new HttpContextBuilder(Application, AllowSynchronousIO, PreserveExecutionContext); builder.Configure((context, reader) => { var request = context.Request; request.Scheme = BaseAddress.Scheme; request.Host = HostString.FromUriComponent(BaseAddress); if (BaseAddress.IsDefaultPort) { request.Host = new HostString(request.Host.Host); } var pathBase = PathString.FromUriComponent(BaseAddress); if (pathBase.HasValue && pathBase.Value.EndsWith('/')) { pathBase = new PathString(pathBase.Value[..^1]); // All but the last character. } request.PathBase = pathBase; }); builder.Configure((context, reader) => configureContext(context)); // TODO: Wrap the request body if any? return await builder.SendAsync(cancellationToken).ConfigureAwait(false); } /// <summary> /// Dispose the <see cref="IWebHost" /> object associated with the test server. /// </summary> public void Dispose() { if (!_disposed) { _disposed = true; _hostInstance?.Dispose(); } } Task IServer.StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) { _application = new ApplicationWrapper<TContext>(application, () => { ObjectDisposedException.ThrowIf(_disposed, this); }); return Task.CompletedTask; } Task IServer.StopAsync(CancellationToken cancellationToken) { return Task.CompletedTask; } }