diff --git a/src/DefaultBuilder/DefaultBuilder.slnf b/src/DefaultBuilder/DefaultBuilder.slnf index c5973eb017d4..f0ea3f2eddf3 100644 --- a/src/DefaultBuilder/DefaultBuilder.slnf +++ b/src/DefaultBuilder/DefaultBuilder.slnf @@ -5,12 +5,6 @@ "src\\DefaultBuilder\\samples\\SampleApp\\DefaultBuilder.SampleApp.csproj", "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.Tests\\Microsoft.AspNetCore.Tests.csproj", "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.FunctionalTests\\Microsoft.AspNetCore.FunctionalTests.csproj", - "src\\DefaultBuilder\\testassets\\CreateDefaultBuilderApp\\CreateDefaultBuilderApp.csproj", - "src\\DefaultBuilder\\testassets\\CreateDefaultBuilderOfTApp\\CreateDefaultBuilderOfTApp.csproj", - "src\\DefaultBuilder\\testassets\\DependencyInjectionApp\\DependencyInjectionApp.csproj", - "src\\DefaultBuilder\\testassets\\StartRequestDelegateUrlApp\\StartRequestDelegateUrlApp.csproj", - "src\\DefaultBuilder\\testassets\\StartRouteBuilderUrlApp\\StartRouteBuilderUrlApp.csproj", - "src\\DefaultBuilder\\testassets\\StartWithIApplicationBuilderUrlApp\\StartWithIApplicationBuilderUrlApp.csproj", "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj" diff --git a/src/DefaultBuilder/samples/SampleApp/Program.cs b/src/DefaultBuilder/samples/SampleApp/Program.cs index 1094fbcdc214..a3d0028efaf1 100644 --- a/src/DefaultBuilder/samples/SampleApp/Program.cs +++ b/src/DefaultBuilder/samples/SampleApp/Program.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -14,17 +15,14 @@ namespace SampleApp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + await using var webApp = WebApplication.Create(args); - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); + webApp.MapGet("/", (Func)(() => "Hello, World!")); + + await webApp.RunAsync(); + } private static void HelloWorld() { @@ -80,8 +78,7 @@ private static void CustomApplicationBuilder() Console.ReadKey(); } } - - private static void StartupClass(string[] args) + private static void DirectWebHost(string[] args) { // Using defaults with a Startup class using (var host = WebHost.CreateDefaultBuilder(args) @@ -107,5 +104,17 @@ private static void HostBuilderWithWebHost(string[] args) host.Run(); } + + private static void DefaultGenericHost(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); } } diff --git a/src/DefaultBuilder/src/BootstrapHostBuilder.cs b/src/DefaultBuilder/src/BootstrapHostBuilder.cs new file mode 100644 index 000000000000..563dba7907a4 --- /dev/null +++ b/src/DefaultBuilder/src/BootstrapHostBuilder.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Hosting +{ + // This exists solely to bootstrap the configuration + internal class BootstrapHostBuilder : IHostBuilder + { + public IDictionary Properties { get; } = new Dictionary(); + private readonly HostBuilderContext _context; + private readonly Configuration _configuration; + private readonly WebHostEnvironment _environment; + + public BootstrapHostBuilder(Configuration configuration, WebHostEnvironment webHostEnvironment) + { + _configuration = configuration; + _environment = webHostEnvironment; + _context = new HostBuilderContext(Properties) + { + Configuration = configuration, + HostingEnvironment = webHostEnvironment + }; + } + + public IHost Build() + { + // HostingHostBuilderExtensions.ConfigureDefaults should never call this. + throw new InvalidOperationException(); + } + + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + configureDelegate(_context, _configuration); + _environment.ApplyConfigurationSettings(_configuration); + _configuration.ChangeBasePath(_environment.ContentRootPath); + return this; + } + + public IHostBuilder ConfigureContainer(Action configureDelegate) + { + // This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that could change in the future. + // If this does get called in the future, it should be called again at a later stage on the ConfigureHostBuilder. + return this; + } + + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + configureDelegate(_configuration); + _environment.ApplyConfigurationSettings(_configuration); + _configuration.ChangeBasePath(_environment.ContentRootPath); + return this; + } + + public IHostBuilder ConfigureServices(Action configureDelegate) + { + // HostingHostBuilderExtensions.ConfigureDefaults calls this via ConfigureLogging + // during the initial config stage. It should be called again later on the ConfigureHostBuilder. + return this; + } + + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) where TContainerBuilder : notnull + { + // This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that chould change in the future. + // If this does get called in the future, it should be called again at a later stage on the ConfigureHostBuilder. + return this; + } + + public IHostBuilder UseServiceProviderFactory(Func> factory) where TContainerBuilder : notnull + { + // HostingHostBuilderExtensions.ConfigureDefaults calls this via UseDefaultServiceProvider + // during the initial config stage. It should be called again later on the ConfigureHostBuilder. + return this; + } + } +} diff --git a/src/DefaultBuilder/src/Configuration.cs b/src/DefaultBuilder/src/Configuration.cs new file mode 100644 index 000000000000..5c3abd260aeb --- /dev/null +++ b/src/DefaultBuilder/src/Configuration.cs @@ -0,0 +1,200 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +// TODO: Microsft.Extensions.Configuration API Proposal +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Configuration is mutable configuration object. It is both a configuration builder and an IConfigurationRoot. + /// As sources are added, it updates its current view of configuration. Once Build is called, configuration is frozen. + /// + public sealed class Configuration : IConfigurationRoot, IConfigurationBuilder + { + private readonly ConfigurationBuilder _builder = new(); + private IConfigurationRoot _configuration; + + /// + /// Gets or sets a configuration value. + /// + /// The configuration key. + /// The configuration value. + public string this[string key] { get => _configuration[key]; set => _configuration[key] = value; } + + /// + /// Gets a configuration sub-section with the specified key. + /// + /// The key of the configuration section. + /// The . + /// + /// This method will never return null. If no matching sub-section is found with the specified key, + /// an empty will be returned. + /// + public IConfigurationSection GetSection(string key) + { + return _configuration.GetSection(key); + } + + /// + /// Gets the immediate descendant configuration sub-sections. + /// + /// The configuration sub-sections. + public IEnumerable GetChildren() => _configuration.GetChildren(); + + IDictionary IConfigurationBuilder.Properties => _builder.Properties; + + // TODO: Handle modifications to Sources and keep the configuration root in sync + IList IConfigurationBuilder.Sources => Sources; + + internal IList Sources { get; } + + IEnumerable IConfigurationRoot.Providers => _configuration.Providers; + + /// + /// Creates a new . + /// + public Configuration() + { + _configuration = _builder.Build(); + + var sources = new ConfigurationSources(_builder.Sources, UpdateConfigurationRoot); + + Sources = sources; + } + + internal void ChangeBasePath(string path) + { + this.SetBasePath(path); + UpdateConfigurationRoot(); + } + + internal void ChangeFileProvider(IFileProvider fileProvider) + { + this.SetFileProvider(fileProvider); + UpdateConfigurationRoot(); + } + + private void UpdateConfigurationRoot() + { + var current = _configuration; + if (current is IDisposable disposable) + { + disposable.Dispose(); + } + _configuration = _builder.Build(); + } + + IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source) + { + Sources.Add(source); + return this; + } + + IConfigurationRoot IConfigurationBuilder.Build() + { + // No more modification is expected after this final build + UpdateConfigurationRoot(); + return this; + } + + IChangeToken IConfiguration.GetReloadToken() + { + // REVIEW: Is this correct? + return _configuration.GetReloadToken(); + } + + void IConfigurationRoot.Reload() + { + _configuration.Reload(); + } + + // On source modifications, we rebuild configuration + private class ConfigurationSources : IList + { + private readonly IList _sources; + private readonly Action _sourcesModified; + + public ConfigurationSources(IList sources, Action sourcesModified) + { + _sources = sources; + _sourcesModified = sourcesModified; + } + + public IConfigurationSource this[int index] + { + get => _sources[index]; + set + { + _sources[index] = value; + _sourcesModified(); + } + } + + public int Count => _sources.Count; + + public bool IsReadOnly => _sources.IsReadOnly; + + public void Add(IConfigurationSource item) + { + _sources.Add(item); + _sourcesModified(); + } + + public void Clear() + { + _sources.Clear(); + _sourcesModified(); + } + + public bool Contains(IConfigurationSource item) + { + return _sources.Contains(item); + } + + public void CopyTo(IConfigurationSource[] array, int arrayIndex) + { + _sources.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return _sources.GetEnumerator(); + } + + public int IndexOf(IConfigurationSource item) + { + return _sources.IndexOf(item); + } + + public void Insert(int index, IConfigurationSource item) + { + _sources.Insert(index, item); + _sourcesModified(); + } + + public bool Remove(IConfigurationSource item) + { + var removed = _sources.Remove(item); + _sourcesModified(); + return removed; + } + + public void RemoveAt(int index) + { + _sources.RemoveAt(index); + _sourcesModified(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + } +} diff --git a/src/DefaultBuilder/src/ConfigureHostBuilder.cs b/src/DefaultBuilder/src/ConfigureHostBuilder.cs new file mode 100644 index 000000000000..00fae15083e8 --- /dev/null +++ b/src/DefaultBuilder/src/ConfigureHostBuilder.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// A non-buildable for . + /// Use to build the . + /// + public sealed class ConfigureHostBuilder : IHostBuilder + { + private Action? _operations; + + /// + public IDictionary Properties { get; } = new Dictionary(); + + internal Configuration Configuration => _configuration; + + private readonly IConfigurationBuilder _hostConfiguration = new ConfigurationBuilder(); + + private readonly WebHostEnvironment _environment; + private readonly Configuration _configuration; + private readonly IServiceCollection _services; + + internal ConfigureHostBuilder(Configuration configuration, WebHostEnvironment environment, IServiceCollection services) + { + _configuration = configuration; + _environment = environment; + _services = services; + } + + IHost IHostBuilder.Build() + { + throw new NotSupportedException($"Call {nameof(WebApplicationBuilder)}.{nameof(WebApplicationBuilder.Build)}() instead."); + } + + /// + public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _operations += b => b.ConfigureAppConfiguration(configureDelegate); + return this; + } + + /// + public IHostBuilder ConfigureContainer(Action configureDelegate) + { + _operations += b => b.ConfigureContainer(configureDelegate); + return this; + } + + /// + public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) + { + // HACK: We need to evaluate the host configuration as they are changes so that we have an accurate view of the world + configureDelegate(_hostConfiguration); + + _environment.ApplyConfigurationSettings(_hostConfiguration.Build()); + Configuration.ChangeBasePath(_environment.ContentRootPath); + + _operations += b => b.ConfigureHostConfiguration(configureDelegate); + return this; + } + + /// + public IHostBuilder ConfigureServices(Action configureDelegate) + { + // Run these immediately so that they are observable by the imperative code + configureDelegate(new HostBuilderContext(Properties) + { + Configuration = Configuration, + HostingEnvironment = _environment + }, + _services); + + return this; + } + + /// + public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) where TContainerBuilder : notnull + { + _operations += b => b.UseServiceProviderFactory(factory); + return this; + } + + /// + public IHostBuilder UseServiceProviderFactory(Func> factory) where TContainerBuilder : notnull + { + _operations += b => b.UseServiceProviderFactory(factory); + return this; + } + + internal void ExecuteActions(IHostBuilder hostBuilder) + { + _operations?.Invoke(hostBuilder); + } + } +} diff --git a/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs new file mode 100644 index 000000000000..5d95319e7738 --- /dev/null +++ b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs @@ -0,0 +1,110 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// A non-buildable for . + /// Use to build the . + /// + public sealed class ConfigureWebHostBuilder : IWebHostBuilder + { + private Action? _operations; + + private readonly WebHostEnvironment _environment; + private readonly Configuration _configuration; + private readonly Dictionary _settings = new(StringComparer.OrdinalIgnoreCase); + private readonly IServiceCollection _services; + + internal ConfigureWebHostBuilder(Configuration configuration, WebHostEnvironment environment, IServiceCollection services) + { + _configuration = configuration; + _environment = environment; + _services = services; + } + + IWebHost IWebHostBuilder.Build() + { + throw new NotSupportedException($"Call {nameof(WebApplicationBuilder)}.{nameof(WebApplicationBuilder.Build)}() instead."); + } + + /// + public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) + { + _operations += b => b.ConfigureAppConfiguration(configureDelegate); + return this; + } + + /// + public IWebHostBuilder ConfigureServices(Action configureServices) + { + configureServices(new WebHostBuilderContext + { + Configuration = _configuration, + HostingEnvironment = _environment + }, + _services); + return this; + } + + /// + public IWebHostBuilder ConfigureServices(Action configureServices) + { + return ConfigureServices((WebHostBuilderContext context, IServiceCollection services) => configureServices(services)); + } + + /// + public string? GetSetting(string key) + { + _settings.TryGetValue(key, out var value); + return value; + } + + /// + public IWebHostBuilder UseSetting(string key, string? value) + { + _settings[key] = value; + _operations += b => b.UseSetting(key, value); + + // All properties on IWebHostEnvironment are non-nullable. + if (value is null) + { + return this; + } + + if (string.Equals(key, WebHostDefaults.ApplicationKey, StringComparison.OrdinalIgnoreCase)) + { + _environment.ApplicationName = value; + } + else if (string.Equals(key, WebHostDefaults.ContentRootKey, StringComparison.OrdinalIgnoreCase)) + { + _environment.ContentRootPath = value; + _environment.ResolveFileProviders(_configuration); + + _configuration.ChangeBasePath(value); + } + else if (string.Equals(key, WebHostDefaults.EnvironmentKey, StringComparison.OrdinalIgnoreCase)) + { + _environment.EnvironmentName = value; + } + else if (string.Equals(key, WebHostDefaults.WebRootKey, StringComparison.OrdinalIgnoreCase)) + { + _environment.WebRootPath = value; + _environment.ResolveFileProviders(_configuration); + } + + return this; + } + + internal void ExecuteActions(IWebHostBuilder webHostBuilder) + { + _operations?.Invoke(webHostBuilder); + } + } +} diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..3c537d7e886b 100644 --- a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt +++ b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt @@ -1 +1,43 @@ #nullable enable +Microsoft.AspNetCore.Builder.Configuration +Microsoft.AspNetCore.Builder.Configuration.Configuration() -> void +Microsoft.AspNetCore.Builder.Configuration.GetChildren() -> System.Collections.Generic.IEnumerable! +Microsoft.AspNetCore.Builder.Configuration.GetSection(string! key) -> Microsoft.Extensions.Configuration.IConfigurationSection! +Microsoft.AspNetCore.Builder.Configuration.this[string! key].get -> string! +Microsoft.AspNetCore.Builder.Configuration.this[string! key].set -> void +Microsoft.AspNetCore.Builder.ConfigureHostBuilder +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.ConfigureAppConfiguration(System.Action! configureDelegate) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.ConfigureContainer(System.Action! configureDelegate) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.ConfigureHostConfiguration(System.Action! configureDelegate) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.ConfigureServices(System.Action! configureDelegate) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.Properties.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.UseServiceProviderFactory(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory! factory) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureHostBuilder.UseServiceProviderFactory(System.Func!>! factory) -> Microsoft.Extensions.Hosting.IHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.ConfigureAppConfiguration(System.Action! configureDelegate) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.ConfigureServices(System.Action! configureServices) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.ConfigureServices(System.Action! configureServices) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.GetSetting(string! key) -> string? +Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder.UseSetting(string! key, string? value) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +Microsoft.AspNetCore.Builder.WebApplication +Microsoft.AspNetCore.Builder.WebApplication.Configuration.get -> Microsoft.Extensions.Configuration.IConfiguration! +Microsoft.AspNetCore.Builder.WebApplication.DisposeAsync() -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Builder.WebApplication.Environment.get -> Microsoft.AspNetCore.Hosting.IWebHostEnvironment! +Microsoft.AspNetCore.Builder.WebApplication.Lifetime.get -> Microsoft.Extensions.Hosting.IHostApplicationLifetime! +Microsoft.AspNetCore.Builder.WebApplication.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.AspNetCore.Builder.WebApplication.Run(string? url = null) -> void +Microsoft.AspNetCore.Builder.WebApplication.RunAsync(string? url = null) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Builder.WebApplication.Services.get -> System.IServiceProvider! +Microsoft.AspNetCore.Builder.WebApplication.StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Builder.WebApplication.StopAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Builder.WebApplication.Urls.get -> System.Collections.Generic.ICollection! +Microsoft.AspNetCore.Builder.WebApplicationBuilder +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Build() -> Microsoft.AspNetCore.Builder.WebApplication! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Configuration.get -> Microsoft.AspNetCore.Builder.Configuration! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Environment.get -> Microsoft.AspNetCore.Hosting.IWebHostEnvironment! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Host.get -> Microsoft.AspNetCore.Builder.ConfigureHostBuilder! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Logging.get -> Microsoft.Extensions.Logging.ILoggingBuilder! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.AspNetCore.Builder.WebApplicationBuilder.WebHost.get -> Microsoft.AspNetCore.Builder.ConfigureWebHostBuilder! +static Microsoft.AspNetCore.Builder.WebApplication.Create(string![]? args = null) -> Microsoft.AspNetCore.Builder.WebApplication! +static Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(string![]? args = null) -> Microsoft.AspNetCore.Builder.WebApplicationBuilder! diff --git a/src/DefaultBuilder/src/WebApplication.cs b/src/DefaultBuilder/src/WebApplication.cs new file mode 100644 index 000000000000..ac5849719b92 --- /dev/null +++ b/src/DefaultBuilder/src/WebApplication.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +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.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// The web application used to configure the HTTP pipeline, and routes. + /// + public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable + { + internal const string EndpointRouteBuilder = "__EndpointRouteBuilder"; + + private readonly IHost _host; + private readonly List _dataSources = new(); + + internal WebApplication(IHost host) + { + _host = host; + ApplicationBuilder = new ApplicationBuilder(host.Services); + Logger = host.Services.GetRequiredService().CreateLogger(Environment.ApplicationName); + } + + /// + /// The application's configured services. + /// + public IServiceProvider Services => _host.Services; + + /// + /// The application's configured . + /// + public IConfiguration Configuration => _host.Services.GetRequiredService(); + + /// + /// The application's configured . + /// + public IWebHostEnvironment Environment => _host.Services.GetRequiredService(); + + /// + /// Allows consumers to be notified of application lifetime events. + /// + public IHostApplicationLifetime Lifetime => _host.Services.GetRequiredService(); + + /// + /// The default logger for the application. + /// + public ILogger Logger { get; } + + /// + /// The list of URLs that the HTTP server is bound to. + /// + public ICollection Urls => ServerFeatures.Get()?.Addresses ?? + throw new InvalidOperationException($"{nameof(IServerAddressesFeature)} could not be found."); + + IServiceProvider IApplicationBuilder.ApplicationServices + { + get => ApplicationBuilder.ApplicationServices; + set => ApplicationBuilder.ApplicationServices = value; + } + + internal IFeatureCollection ServerFeatures => _host.Services.GetRequiredService().Features; + IFeatureCollection IApplicationBuilder.ServerFeatures => ServerFeatures; + + internal IDictionary Properties => ApplicationBuilder.Properties; + IDictionary IApplicationBuilder.Properties => Properties; + + internal ICollection DataSources => _dataSources; + ICollection IEndpointRouteBuilder.DataSources => DataSources; + + internal IEndpointRouteBuilder RouteBuilder + { + get + { + Properties.TryGetValue(EndpointRouteBuilder, out var value); + return (IEndpointRouteBuilder)value!; + } + } + + internal ApplicationBuilder ApplicationBuilder { get; } + + IServiceProvider IEndpointRouteBuilder.ServiceProvider => Services; + + /// + /// Initializes a new instance of the class with preconfigured defaults. + /// + /// Command line arguments + /// The . + public static WebApplication Create(string[]? args = null) => + new WebApplicationBuilder(Assembly.GetCallingAssembly(), args).Build(); + + /// + /// Initializes a new instance of the class with preconfigured defaults. + /// + /// Command line arguments + /// The . + public static WebApplicationBuilder CreateBuilder(string[]? args = null) => + new WebApplicationBuilder(Assembly.GetCallingAssembly(), args); + + /// + /// Start the application. + /// + /// + /// + /// A that represents the startup of the . + /// Successful completion indicates the HTTP server is ready to accept new requests. + /// + public Task StartAsync(CancellationToken cancellationToken = default) => + _host.StartAsync(cancellationToken); + + /// + /// Shuts down the application. + /// + /// + /// + /// A that represents the shutdown of the . + /// Successful completion indicates that all the HTTP server has stopped. + /// + public Task StopAsync(CancellationToken cancellationToken = default) => + _host.StopAsync(cancellationToken); + + /// + /// Runs an application and returns a Task that only completes when the token is triggered or shutdown is triggered. + /// + /// The URL to listen to if the server hasn't been configured directly. + /// + /// A that represents the entire runtime of the from startup to shutdown. + /// + public Task RunAsync(string? url = null) + { + Listen(url); + return HostingAbstractionsHostExtensions.RunAsync(this); + } + + /// + /// Runs an application and block the calling thread until host shutdown. + /// + /// The URL to listen to if the server hasn't been configured directly. + public void Run(string? url = null) + { + Listen(url); + HostingAbstractionsHostExtensions.Run(this); + } + + /// + /// Disposes the application. + /// + void IDisposable.Dispose() => _host.Dispose(); + + /// + /// Disposes the application. + /// + public ValueTask DisposeAsync() => ((IAsyncDisposable)_host).DisposeAsync(); + + internal RequestDelegate BuildRequstDelegate() => ApplicationBuilder.Build(); + RequestDelegate IApplicationBuilder.Build() => BuildRequstDelegate(); + + // REVIEW: Should this be wrapping another type? + IApplicationBuilder IApplicationBuilder.New() => ApplicationBuilder.New(); + + IApplicationBuilder IApplicationBuilder.Use(Func middleware) + { + ApplicationBuilder.Use(middleware); + return this; + } + + IApplicationBuilder IEndpointRouteBuilder.CreateApplicationBuilder() => ApplicationBuilder.New(); + + private void Listen(string? url) + { + if (url is null) + { + return; + } + + var addresses = ServerFeatures.Get()?.Addresses; + if (addresses is null) + { + throw new InvalidOperationException($"Changing the URL is not supported because no valid {nameof(IServerAddressesFeature)} was found."); + } + if (addresses.IsReadOnly) + { + throw new InvalidOperationException($"Changing the URL is not supported because {nameof(IServerAddressesFeature.Addresses)} {nameof(ICollection.IsReadOnly)}."); + } + + addresses.Clear(); + addresses.Add(url); + } + } +} diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs new file mode 100644 index 000000000000..3be41cf94061 --- /dev/null +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -0,0 +1,192 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// A builder for web applications and services. + /// + public sealed class WebApplicationBuilder + { + private readonly HostBuilder _hostBuilder = new(); + private readonly ConfigureHostBuilder _deferredHostBuilder; + private readonly ConfigureWebHostBuilder _deferredWebHostBuilder; + private readonly WebHostEnvironment _environment; + private WebApplication? _builtApplication; + + internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) + { + // HACK: MVC and Identity do this horrible thing to get the hosting environment as an instance + // from the service collection before it is built. That needs to be fixed... + Environment = _environment = new WebHostEnvironment(callingAssembly); + Services.AddSingleton(Environment); + + // Run methods to configure both generic and web host defaults early to populate config from appsettings.json + // environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate + // the correct defaults. + var bootstrapBuilder = new BootstrapHostBuilder(Configuration, _environment); + bootstrapBuilder.ConfigureDefaults(args); + bootstrapBuilder.ConfigureWebHostDefaults(configure: _ => { }); + + Configuration.SetBasePath(_environment.ContentRootPath); + Logging = new LoggingBuilder(Services); + WebHost = _deferredWebHostBuilder = new ConfigureWebHostBuilder(Configuration, _environment, Services); + Host = _deferredHostBuilder = new ConfigureHostBuilder(Configuration, _environment, Services); + + _deferredHostBuilder.ConfigureDefaults(args); + } + + /// + /// Provides information about the web hosting environment an application is running. + /// + public IWebHostEnvironment Environment { get; } + + /// + /// A collection of services for the application to compose. This is useful for adding user provided or framework provided services. + /// + public IServiceCollection Services { get; } = new ServiceCollection(); + + /// + /// A collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers. + /// + public Configuration Configuration { get; } = new(); + + /// + /// A collection of logging providers for the applicaiton to compose. This is useful for adding new logging providers. + /// + public ILoggingBuilder Logging { get; } + + /// + /// An for configuring server specific properties, but not building. + /// To build after configuruation, call . + /// + public ConfigureWebHostBuilder WebHost { get; } + + /// + /// An for configuring host specific properties, but not building. + /// To build after configuration, call . + /// + public ConfigureHostBuilder Host { get; } + + /// + /// Builds the . + /// + /// A configured . + public WebApplication Build() + { + _hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost); + _builtApplication = new WebApplication(_hostBuilder.Build()); + return _builtApplication; + } + + private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app) + { + Debug.Assert(_builtApplication is not null); + + // The endpoints were already added on the outside + if (_builtApplication.DataSources.Count > 0) + { + // The user did not register the routing middleware so wrap the entire + // destination pipeline in UseRouting() and UseEndpoints(), essentially: + // destination.UseRouting() + // destination.Run(source) + // destination.UseEndpoints() + if (_builtApplication.RouteBuilder == null) + { + app.UseRouting(); + + // Copy the route data sources over to the destination pipeline, this should be available since we just called + // UseRouting() + var routes = (IEndpointRouteBuilder)app.Properties[WebApplication.EndpointRouteBuilder]!; + + foreach (var ds in _builtApplication.DataSources) + { + routes.DataSources.Add(ds); + } + + // Chain the execution of the source pipeline into the destination pipeline + app.Use(next => + { + _builtApplication.Run(next); + return _builtApplication.BuildRequstDelegate(); + }); + + // Add a UseEndpoints at the end + app.UseEndpoints(e => { }); + } + else + { + // Since we register routes into the source pipeline's route builder directly, + // if the user called UseRouting, we need to copy the data sources + foreach (var ds in _builtApplication.DataSources) + { + _builtApplication.RouteBuilder.DataSources.Add(ds); + } + + // We then implicitly call UseEndpoints at the end of the pipeline + _builtApplication.UseEndpoints(_ => { }); + + // Wire the source pipeline to run in the destination pipeline + app.Run(_builtApplication.BuildRequstDelegate()); + } + } + else + { + // Wire the source pipeline to run in the destination pipeline + app.Run(_builtApplication.BuildRequstDelegate()); + } + + // Copy the properties to the destination app builder + foreach (var item in _builtApplication.Properties) + { + app.Properties[item.Key] = item.Value; + } + + } + + private void ConfigureWebHost(IWebHostBuilder genericWebHostBuilder) + { + genericWebHostBuilder.Configure(ConfigureApplication); + + _hostBuilder.ConfigureServices((context, services) => + { + foreach (var s in Services) + { + services.Add(s); + } + }); + + _hostBuilder.ConfigureAppConfiguration((hostContext, builder) => + { + foreach (var s in Configuration.Sources) + { + builder.Sources.Add(s); + } + }); + + _deferredHostBuilder.ExecuteActions(_hostBuilder); + _deferredWebHostBuilder.ExecuteActions(genericWebHostBuilder); + + _environment.ApplyEnvironmentSettings(genericWebHostBuilder); + } + + private class LoggingBuilder : ILoggingBuilder + { + public LoggingBuilder(IServiceCollection services) + { + Services = services; + } + + public IServiceCollection Services { get; } + } + } +} diff --git a/src/DefaultBuilder/src/WebHostEnvironment.cs b/src/DefaultBuilder/src/WebHostEnvironment.cs new file mode 100644 index 000000000000..828a59c6f62c --- /dev/null +++ b/src/DefaultBuilder/src/WebHostEnvironment.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.StaticWebAssets; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; + +namespace Microsoft.AspNetCore.Builder +{ + internal class WebHostEnvironment : IWebHostEnvironment + { + private static readonly NullFileProvider NullFileProvider = new(); + + public WebHostEnvironment(Assembly? callingAssembly) + { + ContentRootPath = Directory.GetCurrentDirectory(); + + ApplicationName = (callingAssembly ?? Assembly.GetEntryAssembly())?.GetName()?.Name ?? string.Empty; + EnvironmentName = Environments.Production; + + // This feels wrong, but HostingEnvironment also sets WebRoot to "default!". + WebRootPath = default!; + + // Default to /wwwroot if it exists. + var wwwroot = Path.Combine(ContentRootPath, "wwwroot"); + if (Directory.Exists(wwwroot)) + { + WebRootPath = wwwroot; + } + + ContentRootFileProvider = NullFileProvider; + WebRootFileProvider = NullFileProvider; + + ResolveFileProviders(new Configuration()); + } + + public void ApplyConfigurationSettings(IConfiguration configuration) + { + ApplicationName = configuration[WebHostDefaults.ApplicationKey] ?? ApplicationName; + ContentRootPath = configuration[WebHostDefaults.ContentRootKey] ?? ContentRootPath; + EnvironmentName = configuration[WebHostDefaults.EnvironmentKey] ?? EnvironmentName; + WebRootPath = configuration[WebHostDefaults.ContentRootKey] ?? WebRootPath; + + ResolveFileProviders(configuration); + } + + public void ApplyEnvironmentSettings(IWebHostBuilder genericWebHostBuilder) + { + genericWebHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, ApplicationName); + genericWebHostBuilder.UseSetting(WebHostDefaults.EnvironmentKey, EnvironmentName); + genericWebHostBuilder.UseSetting(WebHostDefaults.ContentRootKey, ContentRootPath); + genericWebHostBuilder.UseSetting(WebHostDefaults.WebRootKey, WebRootPath); + } + + public void ResolveFileProviders(IConfiguration configuration) + { + if (Directory.Exists(ContentRootPath)) + { + ContentRootFileProvider = new PhysicalFileProvider(ContentRootPath); + } + + if (Directory.Exists(WebRootPath)) + { + WebRootFileProvider = new PhysicalFileProvider(Path.Combine(ContentRootPath, WebRootPath)); + } + + if (this.IsDevelopment()) + { + StaticWebAssetsLoader.UseStaticWebAssets(this, configuration); + } + } + + public string ApplicationName { get; set; } + public string EnvironmentName { get; set; } + + public IFileProvider ContentRootFileProvider { get; set; } + public string ContentRootPath { get; set; } + + public IFileProvider WebRootFileProvider { get; set; } + public string WebRootPath { get; set; } + } +} diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs new file mode 100644 index 000000000000..53273e7c77da --- /dev/null +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace Microsoft.AspNetCore.Tests +{ + public class WebApplicationFunctionalTests : LoggedTest + { + [Fact] + public async Task LoggingConfigurationSectionPassedToLoggerByDefault() + { + try + { + await File.WriteAllTextAsync("appsettings.json", @" +{ + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Warning"" + } + } +}"); + + await using var app = WebApplication.Create(); + + var factory = (ILoggerFactory)app.Services.GetService(typeof(ILoggerFactory)); + var logger = factory.CreateLogger("Test"); + + logger.Log(LogLevel.Information, 0, "Message", null, (s, e) => + { + Assert.True(false); + return string.Empty; + }); + + var logWritten = false; + logger.Log(LogLevel.Warning, 0, "Message", null, (s, e) => + { + logWritten = true; + return string.Empty; + }); + + Assert.True(logWritten); + } + finally + { + File.Delete("appsettings.json"); + } + } + } +} diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs new file mode 100644 index 000000000000..dcd8e0b865c9 --- /dev/null +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -0,0 +1,407 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.HostFiltering; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Hosting.Server.Features; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.Tests +{ + public class WebApplicationTests + { + [Fact] + public async Task WebApplicationBuilderConfiguration_IncludesCommandLineArguments() + { + var builder = WebApplication.CreateBuilder(new string[] { "--urls", "http://localhost:5001" }); + Assert.Equal("http://localhost:5001", builder.Configuration["urls"]); + + var urls = new List(); + var server = new MockAddressesServer(urls); + builder.Services.AddSingleton(server); + await using var app = builder.Build(); + + await app.StartAsync(); + + var address = Assert.Single(urls); + Assert.Equal("http://localhost:5001", address); + + Assert.Same(app.Urls, urls); + + var url = Assert.Single(urls); + Assert.Equal("http://localhost:5001", url); + } + + [Fact] + public async Task WebApplicationRunAsync_UsesDefaultUrls() + { + var builder = WebApplication.CreateBuilder(); + var urls = new List(); + var server = new MockAddressesServer(urls); + builder.Services.AddSingleton(server); + await using var app = builder.Build(); + + await app.StartAsync(); + + Assert.Same(app.Urls, urls); + + Assert.Equal(2, urls.Count); + Assert.Equal("http://localhost:5000", urls[0]); + Assert.Equal("https://localhost:5001", urls[1]); + } + + [Fact] + public async Task WebApplicationRunUrls_UpdatesIServerAddressesFeature() + { + var builder = WebApplication.CreateBuilder(); + var urls = new List(); + var server = new MockAddressesServer(urls); + builder.Services.AddSingleton(server); + await using var app = builder.Build(); + + var runTask = app.RunAsync("http://localhost:5001"); + + var url = Assert.Single(urls); + Assert.Equal("http://localhost:5001", url); + + await app.StopAsync(); + await runTask; + } + + [Fact] + public async Task WebApplicationUrls_UpdatesIServerAddressesFeature() + { + var builder = WebApplication.CreateBuilder(); + var urls = new List(); + var server = new MockAddressesServer(urls); + builder.Services.AddSingleton(server); + await using var app = builder.Build(); + + app.Urls.Add("http://localhost:5002"); + app.Urls.Add("https://localhost:5003"); + + await app.StartAsync(); + + Assert.Equal(2, urls.Count); + Assert.Equal("http://localhost:5002", urls[0]); + Assert.Equal("https://localhost:5003", urls[1]); + } + + [Fact] + public async Task WebApplicationRunUrls_OverridesIServerAddressesFeature() + { + var builder = WebApplication.CreateBuilder(); + var urls = new List(); + var server = new MockAddressesServer(urls); + builder.Services.AddSingleton(server); + await using var app = builder.Build(); + + app.Urls.Add("http://localhost:5002"); + app.Urls.Add("https://localhost:5003"); + + var runTask = app.RunAsync("http://localhost:5001"); + + var url = Assert.Single(urls); + Assert.Equal("http://localhost:5001", url); + + await app.StopAsync(); + await runTask; + } + + [Fact] + public async Task WebApplicationUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddSingleton(new MockAddressesServer()); + await using var app = builder.Build(); + + Assert.Throws(() => app.Urls); + } + + [Fact] + public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfThereIsNoIServerAddressesFeature() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddSingleton(new MockAddressesServer()); + await using var app = builder.Build(); + + await Assert.ThrowsAsync(() => app.RunAsync("http://localhost:5001")); + } + + [Fact] + public async Task WebApplicationRunUrls_ThrowsInvalidOperationExceptionIfServerAddressesFeatureIsReadOnly() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddSingleton(new MockAddressesServer(new List().AsReadOnly())); + await using var app = builder.Build(); + + await Assert.ThrowsAsync(() => app.RunAsync("http://localhost:5001")); + } + + [Fact] + public void WebApplicationBuilderHost_ThrowsWhenBuiltDirectly() + { + Assert.Throws(() => ((IHostBuilder)WebApplication.CreateBuilder().Host).Build()); + } + + [Fact] + public void WebApplicationBuilderWebHost_ThrowsWhenBuiltDirectly() + { + Assert.Throws(() => ((IWebHostBuilder)WebApplication.CreateBuilder().WebHost).Build()); + } + + [Fact] + public void WebApplicationBuilderWebHostUseSettings_IsCaseInsensitive() + { + var builder = WebApplication.CreateBuilder(); + + var contentRoot = Path.GetTempPath().ToString(); + var webRoot = Path.GetTempPath().ToString(); + var envName = $"{nameof(WebApplicationTests)}_ENV"; + + builder.WebHost.UseSetting("applicationname", nameof(WebApplicationTests)); + + builder.WebHost.UseSetting("ENVIRONMENT", envName); + builder.WebHost.UseSetting("CONTENTROOT", contentRoot); + builder.WebHost.UseSetting("WEBROOT", webRoot); + + Assert.Equal(nameof(WebApplicationTests), builder.WebHost.GetSetting("APPLICATIONNAME")); + Assert.Equal(envName, builder.WebHost.GetSetting("environment")); + Assert.Equal(contentRoot, builder.WebHost.GetSetting("contentroot")); + Assert.Equal(webRoot, builder.WebHost.GetSetting("webroot")); + + var app = builder.Build(); + + Assert.Equal(nameof(WebApplicationTests), app.Environment.ApplicationName); + Assert.Equal(envName, app.Environment.EnvironmentName); + Assert.Equal(contentRoot, app.Environment.ContentRootPath); + Assert.Equal(webRoot, app.Environment.WebRootPath); + } + + [Fact] + public void WebApplicationBuilderHostProperties_IsCaseSensitive() + { + var builder = WebApplication.CreateBuilder(); + + builder.Host.Properties["lowercase"] = nameof(WebApplicationTests); + + Assert.Equal(nameof(WebApplicationTests), builder.Host.Properties["lowercase"]); + Assert.False(builder.Host.Properties.ContainsKey("Lowercase")); + } + + [Fact] + public async Task WebApplicationConfiguration_HostFilterOptionsAreReloadable() + { + var builder = WebApplication.CreateBuilder(); + var host = builder.WebHost + .ConfigureAppConfiguration(configBuilder => + { + configBuilder.Add(new ReloadableMemorySource()); + }); + await using var app = builder.Build(); + + var config = app.Services.GetRequiredService(); + var monitor = app.Services.GetRequiredService>(); + var options = monitor.CurrentValue; + + Assert.Contains("*", options.AllowedHosts); + + var changed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + monitor.OnChange(newOptions => + { + changed.SetResult(0); + }); + + config["AllowedHosts"] = "NewHost"; + + await changed.Task.TimeoutAfter(TimeSpan.FromSeconds(10)); + options = monitor.CurrentValue; + Assert.Contains("NewHost", options.AllowedHosts); + } + + [Fact] + public async Task WebApplicationConfiguration_EnablesForwardedHeadersFromConfig() + { + var builder = WebApplication.CreateBuilder(); + builder.WebHost.UseTestServer(); + builder.Configuration.AddInMemoryCollection(new[] + { + new KeyValuePair("FORWARDEDHEADERS_ENABLED", "true" ), + }); + await using var app = builder.Build(); + + app.Run(context => + { + Assert.Equal("https", context.Request.Scheme); + return Task.CompletedTask; + }); + + await app.StartAsync(); + var client = app.GetTestClient(); + client.DefaultRequestHeaders.Add("x-forwarded-proto", "https"); + var result = await client.GetAsync("http://localhost/"); + result.EnsureSuccessStatusCode(); + } + + [Fact] + public void WebApplicationCreate_RegistersRouting() + { + var app = WebApplication.Create(); + var linkGenerator = app.Services.GetService(typeof(LinkGenerator)); + Assert.NotNull(linkGenerator); + } + + [Fact] + public void WebApplicationCreate_RegistersEventSourceLogger() + { + var listener = new TestEventListener(); + var app = WebApplication.Create(); + + var logger = app.Services.GetRequiredService>(); + var guid = Guid.NewGuid().ToString(); + logger.LogInformation(guid); + + var events = listener.EventData.ToArray(); + Assert.Contains(events, args => + args.EventSource.Name == "Microsoft-Extensions-Logging" && + args.Payload.OfType().Any(p => p.Contains(guid))); + } + + [Fact] + public void WebApplicationBuilder_CanClearDefaultLoggers() + { + var listener = new TestEventListener(); + var builder = WebApplication.CreateBuilder(); + builder.Logging.ClearProviders(); + + var app = builder.Build(); + + var logger = app.Services.GetRequiredService>(); + var guid = Guid.NewGuid().ToString(); + logger.LogInformation(guid); + + var events = listener.EventData.ToArray(); + Assert.DoesNotContain(events, args => + args.EventSource.Name == "Microsoft-Extensions-Logging" && + args.Payload.OfType().Any(p => p.Contains(guid))); + } + + private class TestEventListener : EventListener + { + private volatile bool _disposed; + + private ConcurrentQueue _events = new ConcurrentQueue(); + + public IEnumerable EventData => _events; + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name == "Microsoft-Extensions-Logging") + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (!_disposed) + { + _events.Enqueue(eventData); + } + } + + public override void Dispose() + { + _disposed = true; + base.Dispose(); + } + } + + private class ReloadableMemorySource : IConfigurationSource + { + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new ReloadableMemoryProvider(); + } + } + + private class ReloadableMemoryProvider : ConfigurationProvider + { + public override void Set(string key, string value) + { + base.Set(key, value); + OnReload(); + } + } + + private class MockAddressesServer : IServer + { + private readonly ICollection _urls; + + public MockAddressesServer() + { + // For testing a server that doesn't set an IServerAddressesFeature. + } + + public MockAddressesServer(ICollection urls) + { + _urls = urls; + + var mockAddressesFeature = new MockServerAddressesFeature + { + Addresses = urls + }; + + Features.Set(mockAddressesFeature); + } + + public IFeatureCollection Features { get; } = new FeatureCollection(); + + public Task StartAsync(IHttpApplication application, CancellationToken cancellationToken) where TContext : notnull + { + if (_urls.Count == 0) + { + // This is basically Kestrel's DefaultAddressStrategy. + _urls.Add("http://localhost:5000"); + _urls.Add("https://localhost:5001"); + } + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public void Dispose() + { + } + + private class MockServerAddressesFeature : IServerAddressesFeature + { + public ICollection Addresses { get; set; } + public bool PreferHostingUrls { get; set; } + } + } + } +} diff --git a/src/Http/samples/MinimalSample/Program.cs b/src/Http/samples/MinimalSample/Program.cs index 40697fdc6664..e83891ce94ed 100644 --- a/src/Http/samples/MinimalSample/Program.cs +++ b/src/Http/samples/MinimalSample/Program.cs @@ -1,35 +1,20 @@ using System; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using var host = Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.Configure(app => - { - app.UseRouting(); +await using var app = WebApplication.Create(); - app.UseEndpoints(endpoints => - { - Todo EchoTodo([FromBody] Todo todo) => todo; - endpoints.MapPost("/EchoTodo", (Func)EchoTodo); +Todo EchoTodo(Todo todo) => todo; +app.MapPost("/EchoTodo", (Func)EchoTodo); - string Plaintext() => "Hello, World!"; - endpoints.MapGet("/plaintext", (Func)Plaintext); +string Plaintext() => "Hello, World!"; +app.MapGet("/plaintext", (Func)Plaintext); - object Json() => new { message = "Hello, World!" }; - endpoints.MapGet("/json", (Func)Json); +object Json() => new { message = "Hello, World!" }; +app.MapGet("/json", (Func)Json); - string SayHello(string name) => $"Hello {name}"; - endpoints.MapGet("/hello/{name}", (Func)SayHello); - }); - }); - }) - .Build(); +string SayHello(string name) => $"Hello {name}"; +app.MapGet("/hello/{name}", (Func)SayHello); -await host.RunAsync(); +await app.RunAsync(); record Todo(int Id, string Name, bool IsComplete);