From 3111032dc074a252f1d1cbd951ecb41153bd1555 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Sun, 19 Jan 2020 15:51:50 -0800 Subject: [PATCH] Rework of Blazor startup experience Fixes: #16874 This is a significant simplication of our startup code model for Blazor wasm with the goal of removing concepts that don't make much sense here. Previously in this area we've tried to be consistent with ASP.NET Core on the server, but it's not helping up much in WASM. We're still leveraging some of the lessons from server-size ASP.NET (hello CreateDefaultBuilder) but consistency is no longer a goal. This change actually makes a bunch of scenarios better (rather than removing features) - it's now possible to access services from the application's DI scope and initialize them before the UI is shown `RunAsync`. This change also adds configuration in a central way. There's nothing in this change that populates configuration in an automatic way, that will come next. --- .../ref/Microsoft.AspNetCore.Blazor.csproj | 2 +- ...rosoft.AspNetCore.Blazor.netstandard2.1.cs | 62 +++--- .../ComponentsApplicationBuilderExtensions.cs | 24 -- .../Builder/IComponentsApplicationBuilder.cs | 26 --- .../src/Hosting/BlazorWebAssemblyHost.cs | 20 -- .../src/Hosting/ConventionBasedStartup.cs | 117 ---------- .../Blazor/src/Hosting/IBlazorStartup.cs | 16 -- .../Blazor/src/Hosting/IWebAssemblyHost.cs | 34 --- .../src/Hosting/IWebAssemblyHostBuilder.cs | 46 ---- .../IWebAssemblyServiceFactoryAdapter.cs | 17 -- .../src/Hosting/RootComponentMapping.cs | 53 +++++ .../Hosting/RootComponentMappingCollection.cs | 68 ++++++ .../WebAssemblyBlazorApplicationBuilder.cs | 54 ----- .../Blazor/src/Hosting/WebAssemblyHost.cs | 171 ++++++++------ .../src/Hosting/WebAssemblyHostBuilder.cs | 136 ++++++------ .../Hosting/WebAssemblyHostBuilderContext.cs | 27 --- .../WebAssemblyHostBuilderExtensions.cs | 73 ------ .../src/Hosting/WebAssemblyHostExtensions.cs | 36 --- .../WebAssemblyServiceFactoryAdapter.cs | 52 ----- .../src/Microsoft.AspNetCore.Blazor.csproj | 4 +- .../Hosting/ConventionBasedStartupTest.cs | 210 ------------------ .../test/Hosting/RootComponentMappingTest.cs | 37 +++ .../Hosting/WebAssemblyHostBuilderTest.cs | 184 ++++++--------- .../test/Hosting/WebAssemblyHostTest.cs | 81 ++++--- .../HostedInAspNet.Client/Program.cs | 12 +- .../HostedInAspNet.Client/Startup.cs | 20 -- .../testassets/StandaloneApp/Program.cs | 12 +- .../testassets/StandaloneApp/Startup.cs | 20 -- .../Wasm.Performance/Driver/Program.cs | 4 +- .../Wasm.Performance/TestApp/Program.cs | 12 +- .../Wasm.Performance/TestApp/Startup.cs | 20 -- .../test/testassets/BasicTestApp/Program.cs | 29 ++- .../test/testassets/BasicTestApp/Startup.cs | 39 ---- .../test/testassets/TestServer/CorsStartup.cs | 4 +- .../TestServer/InternationalizationStartup.cs | 2 +- ...tartupWithMapFallbackToClientSideBlazor.cs | 10 +- .../BlazorWasm-CSharp/Client/Program.cs | 28 ++- .../BlazorWasm-CSharp/Client/Startup.cs | 28 --- .../BlazorWasm-CSharp/Server/Startup.cs | 4 +- 39 files changed, 545 insertions(+), 1249 deletions(-) delete mode 100644 src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs delete mode 100644 src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs create mode 100644 src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs create mode 100644 src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs delete mode 100644 src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs delete mode 100644 src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs create mode 100644 src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs delete mode 100644 src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs delete mode 100644 src/Components/Blazor/testassets/StandaloneApp/Startup.cs delete mode 100644 src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs delete mode 100644 src/Components/test/testassets/BasicTestApp/Startup.cs delete mode 100644 src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Startup.cs diff --git a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.csproj index 88ca078caa..25d2991601 100644 --- a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.csproj @@ -7,6 +7,6 @@ - + diff --git a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs index b8062d72c0..eb33362975 100644 --- a/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs +++ b/src/Components/Blazor/Blazor/ref/Microsoft.AspNetCore.Blazor.netstandard2.1.cs @@ -12,38 +12,38 @@ namespace Microsoft.AspNetCore.Blazor } namespace Microsoft.AspNetCore.Blazor.Hosting { - public static partial class BlazorWebAssemblyHost + [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] + public readonly partial struct RootComponentMapping { - public static Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder CreateDefaultBuilder() { throw null; } + private readonly object _dummy; + public RootComponentMapping(System.Type componentType, string selector) { throw null; } + public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public string Selector { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } } - public partial interface IWebAssemblyHost : System.IDisposable + public partial class RootComponentMappingCollection : System.Collections.ObjectModel.Collection { - System.IServiceProvider Services { get; } - System.Threading.Tasks.Task StartAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); - System.Threading.Tasks.Task StopAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)); + public RootComponentMappingCollection() { } + public void Add(System.Type componentType, string selector) { } + public void AddRange(System.Collections.Generic.IEnumerable items) { } + public void Add(string selector) where TComponent : Microsoft.AspNetCore.Components.IComponent { } } - public partial interface IWebAssemblyHostBuilder + public sealed partial class WebAssemblyHost : System.IAsyncDisposable { - System.Collections.Generic.IDictionary Properties { get; } - Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHost Build(); - Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder ConfigureServices(System.Action configureDelegate); - Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseServiceProviderFactory(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory factory); - Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseServiceProviderFactory(System.Func> factory); + internal WebAssemblyHost() { } + public Microsoft.Extensions.Configuration.IConfiguration Configuration { get { throw null; } } + public System.IServiceProvider Services { get { throw null; } } + [System.Diagnostics.DebuggerStepThroughAttribute] + public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; } + public System.Threading.Tasks.Task RunAsync() { throw null; } } - public sealed partial class WebAssemblyHostBuilderContext + public sealed partial class WebAssemblyHostBuilder { - public WebAssemblyHostBuilderContext(System.Collections.Generic.IDictionary properties) { } - public System.Collections.Generic.IDictionary Properties { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } - } - public static partial class WebAssemblyHostBuilderExtensions - { - public static Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder ConfigureServices(this Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder hostBuilder, System.Action configureDelegate) { throw null; } - public static Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseBlazorStartup(this Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder builder, System.Type startupType) { throw null; } - public static Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseBlazorStartup(this Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder builder) { throw null; } - } - public static partial class WebAssemblyHostExtensions - { - public static void Run(this Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHost host) { } + internal WebAssemblyHostBuilder() { } + public Microsoft.Extensions.Configuration.IConfigurationBuilder Configuration { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.AspNetCore.Blazor.Hosting.RootComponentMappingCollection RootComponents { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } + public Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHost Build() { throw null; } + public static Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHostBuilder CreateDefault(string[] args = null) { throw null; } } } namespace Microsoft.AspNetCore.Blazor.Http @@ -67,15 +67,3 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public static System.Threading.Tasks.Task DispatchEvent(Microsoft.AspNetCore.Components.RenderTree.WebEventDescriptor eventDescriptor, string eventArgsJson) { throw null; } } } -namespace Microsoft.AspNetCore.Components.Builder -{ - public static partial class ComponentsApplicationBuilderExtensions - { - public static void AddComponent(this Microsoft.AspNetCore.Components.Builder.IComponentsApplicationBuilder app, string domElementSelector) where TComponent : Microsoft.AspNetCore.Components.IComponent { } - } - public partial interface IComponentsApplicationBuilder - { - System.IServiceProvider Services { get; } - void AddComponent(System.Type componentType, string domElementSelector); - } -} diff --git a/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs b/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs deleted file mode 100644 index 66532474c6..0000000000 --- a/src/Components/Blazor/Blazor/src/Builder/ComponentsApplicationBuilderExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.Components.Builder -{ - /// - /// Provides extension methods for . - /// - public static class ComponentsApplicationBuilderExtensions - { - /// - /// Associates the component type with the application, - /// causing it to be displayed in the specified DOM element. - /// - /// The . - /// The type of the component. - /// A CSS selector that uniquely identifies a DOM element. - public static void AddComponent(this IComponentsApplicationBuilder app, string domElementSelector) - where TComponent : IComponent - { - app.AddComponent(typeof(TComponent), domElementSelector); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs b/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs deleted file mode 100644 index 84806a8769..0000000000 --- a/src/Components/Blazor/Blazor/src/Builder/IComponentsApplicationBuilder.cs +++ /dev/null @@ -1,26 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Components.Builder -{ - /// - /// A builder for adding components to an application. - /// - public interface IComponentsApplicationBuilder - { - /// - /// Gets the application services. - /// - IServiceProvider Services { get; } - - /// - /// Associates the with the application, - /// causing it to be displayed in the specified DOM element. - /// - /// The type of the component. - /// A CSS selector that uniquely identifies a DOM element. - void AddComponent(Type componentType, string domElementSelector); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs deleted file mode 100644 index e5631805ef..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/BlazorWebAssemblyHost.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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. - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// Used to create an instance of Blazor host builder for a Browser application. - /// - public static class BlazorWebAssemblyHost - { - /// - /// Creates an instance of . - /// - /// The . - public static IWebAssemblyHostBuilder CreateDefaultBuilder() - { - return new WebAssemblyHostBuilder(); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs b/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs deleted file mode 100644 index 53eecbf6e0..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/ConventionBasedStartup.cs +++ /dev/null @@ -1,117 +0,0 @@ -// 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.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Runtime.ExceptionServices; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - // Keeping this simple for now to focus on predictable and reasonable behaviors. - // Startup in WebHost supports lots of things we don't yet support, and some we - // may never support. - // - // Possible additions: - // - environments - // - case-insensitivity (makes sense with environments) - // - // Likely never: - // - statics - // - DI into constructor - internal class ConventionBasedStartup : IBlazorStartup - { - public ConventionBasedStartup(object instance) - { - Instance = instance ?? throw new ArgumentNullException(nameof(instance)); - } - - public object Instance { get; } - - public void Configure(IComponentsApplicationBuilder app, IServiceProvider services) - { - try - { - var method = GetConfigureMethod(); - Debug.Assert(method != null); - - var parameters = method.GetParameters(); - var arguments = new object[parameters.Length]; - for (var i = 0; i < parameters.Length; i++) - { - var parameter = parameters[i]; - arguments[i] = parameter.ParameterType == typeof(IComponentsApplicationBuilder) - ? app - : services.GetRequiredService(parameter.ParameterType); - } - - method.Invoke(Instance, arguments); - } - catch (Exception ex) - { - if (ex is TargetInvocationException) - { - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - } - - throw; - } - } - - internal MethodInfo GetConfigureMethod() - { - var methods = Instance.GetType() - .GetMethods(BindingFlags.Instance | BindingFlags.Public) - .Where(m => string.Equals(m.Name, "Configure", StringComparison.Ordinal)) - .ToArray(); - - if (methods.Length == 1) - { - return methods[0]; - } - else if (methods.Length == 0) - { - throw new InvalidOperationException("The startup class must define a 'Configure' method."); - } - else - { - throw new InvalidOperationException("Overloading the 'Configure' method is not supported."); - } - } - - public void ConfigureServices(IServiceCollection services) - { - try - { - var method = GetConfigureServicesMethod(); - if (method != null) - { - method.Invoke(Instance, new object[] { services }); - } - } - catch (Exception ex) - { - if (ex is TargetInvocationException) - { - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - } - - throw; - } - } - - internal MethodInfo GetConfigureServicesMethod() - { - return Instance.GetType() - .GetMethod( - "ConfigureServices", - BindingFlags.Public | BindingFlags.Instance, - null, - new Type[] { typeof(IServiceCollection), }, - Array.Empty()); - } - } -} \ No newline at end of file diff --git a/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs b/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs deleted file mode 100644 index e87138387f..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IBlazorStartup.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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 Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - internal interface IBlazorStartup - { - void ConfigureServices(IServiceCollection services); - - void Configure(IComponentsApplicationBuilder app, IServiceProvider services); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs deleted file mode 100644 index 8f21f93421..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHost.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.Threading; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// A program abstraction. - /// - public interface IWebAssemblyHost : IDisposable - { - /// - /// The programs configured services. - /// - IServiceProvider Services { get; } - - /// - /// Start the program. - /// - /// Used to abort program start. - /// - Task StartAsync(CancellationToken cancellationToken = default); - - /// - /// Attempts to gracefully stop the program. - /// - /// Used to indicate when stop should no longer be graceful. - /// - Task StopAsync(CancellationToken cancellationToken = default); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs deleted file mode 100644 index c5a52bc280..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyHostBuilder.cs +++ /dev/null @@ -1,46 +0,0 @@ -// 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.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// Abstraction for configuring a Blazor browser-based application. - /// - public interface IWebAssemblyHostBuilder - { - /// - /// A central location for sharing state between components during the host building process. - /// - IDictionary Properties { get; } - - /// - /// Overrides the factory used to create the service provider. - /// - /// The same instance of the for chaining. - IWebAssemblyHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory); - - /// - /// Overrides the factory used to create the service provider. - /// - /// The same instance of the for chaining. - IWebAssemblyHostBuilder UseServiceProviderFactory(Func> factory); - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// The delegate for configuring the that will be used - /// to construct the . - /// The same instance of the for chaining. - IWebAssemblyHostBuilder ConfigureServices(Action configureDelegate); - - /// - /// Run the given actions to initialize the host. This can only be called once. - /// - /// An initialized - IWebAssemblyHost Build(); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs b/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs deleted file mode 100644 index c790a3c879..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/IWebAssemblyServiceFactoryAdapter.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - // Equivalent to https://github.com/aspnet/Extensions/blob/master/src/Hosting/Hosting/src/Internal/IServiceFactoryAdapter.cs - - internal interface IWebAssemblyServiceFactoryAdapter - { - object CreateBuilder(IServiceCollection services); - - IServiceProvider CreateServiceProvider(object containerBuilder); - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs new file mode 100644 index 0000000000..53b34d8822 --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMapping.cs @@ -0,0 +1,53 @@ +// 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 Microsoft.AspNetCore.Components; + +namespace Microsoft.AspNetCore.Blazor.Hosting +{ + /// + /// Defines a mapping between a root and a DOM element selector. + /// + public readonly struct RootComponentMapping + { + /// + /// Creates a new instance of with the provided + /// and . + /// + /// The component type. Must implement . + /// The DOM element selector. + public RootComponentMapping(Type componentType, string selector) + { + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (!typeof(IComponent).IsAssignableFrom(componentType)) + { + throw new ArgumentException( + $"The type '{componentType.Name}' must implement {nameof(IComponent)} to be used as a root component.", + nameof(componentType)); + } + + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + ComponentType = componentType; + Selector = selector; + } + + /// + /// Gets the component type. + /// + public Type ComponentType { get; } + + /// + /// Gets the DOM element selector. + /// + public string Selector { get; } + } +} diff --git a/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs new file mode 100644 index 0000000000..0e488f0deb --- /dev/null +++ b/src/Components/Blazor/Blazor/src/Hosting/RootComponentMappingCollection.cs @@ -0,0 +1,68 @@ +// 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.Collections.ObjectModel; +using Microsoft.AspNetCore.Components; + +namespace Microsoft.AspNetCore.Blazor.Hosting +{ + /// + /// Defines a collection of items. + /// + public class RootComponentMappingCollection : Collection + { + /// + /// Adds a component mapping to the collection. + /// + /// The component type. + /// The DOM element selector. + public void Add(string selector) where TComponent : IComponent + { + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + Add(new RootComponentMapping(typeof(TComponent), selector)); + } + + /// + /// Adds a component mapping to the collection. + /// + /// The component type. Must implement . + /// The DOM element selector. + public void Add(Type componentType, string selector) + { + if (componentType is null) + { + throw new ArgumentNullException(nameof(componentType)); + } + + if (selector is null) + { + throw new ArgumentNullException(nameof(selector)); + } + + Add(new RootComponentMapping(componentType, selector)); + } + + /// + /// Adds a collection of items to this collection. + /// + /// The items to add. + public void AddRange(IEnumerable items) + { + if (items is null) + { + throw new ArgumentNullException(nameof(items)); + } + + foreach (var item in items) + { + Add(item); + } + } + } +} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs deleted file mode 100644 index 8cb5d87561..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyBlazorApplicationBuilder.cs +++ /dev/null @@ -1,54 +0,0 @@ -// 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.Threading.Tasks; -using Microsoft.AspNetCore.Blazor.Rendering; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - internal class WebAssemblyBlazorApplicationBuilder : IComponentsApplicationBuilder - { - public WebAssemblyBlazorApplicationBuilder(IServiceProvider services) - { - Entries = new List<(Type componentType, string domElementSelector)>(); - Services = services; - } - - public List<(Type componentType, string domElementSelector)> Entries { get; } - - public IServiceProvider Services { get; } - - public void AddComponent(Type componentType, string domElementSelector) - { - if (componentType == null) - { - throw new ArgumentNullException(nameof(componentType)); - } - - if (domElementSelector == null) - { - throw new ArgumentNullException(nameof(domElementSelector)); - } - - Entries.Add((componentType, domElementSelector)); - } - - public async Task CreateRendererAsync() - { - var loggerFactory = (ILoggerFactory)Services.GetService(typeof(ILoggerFactory)); - var renderer = new WebAssemblyRenderer(Services, loggerFactory); - for (var i = 0; i < Entries.Count; i++) - { - var (componentType, domElementSelector) = Entries[i]; - await renderer.AddComponentAsync(componentType, domElementSelector); - } - - return renderer; - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs index b90878fdde..02c2693ff7 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHost.cs @@ -5,94 +5,135 @@ using System; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Rendering; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.JSInterop; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Blazor.Hosting { - internal class WebAssemblyHost : IWebAssemblyHost + /// + /// A host object for Blazor running under WebAssembly. Use + /// to initialize a . + /// + public sealed class WebAssemblyHost : IAsyncDisposable { - private readonly IJSRuntime _runtime; + private readonly IServiceScope _scope; + private readonly IServiceProvider _services; + private readonly IConfiguration _configuration; + private readonly RootComponentMapping[] _rootComponents; - private IServiceScope _scope; + // NOTE: the host is disposable because it OWNs references to disposable things. + // + // The twist is that in general dispose is not going to run even if the user puts it in a using. + // When a user refreshes or navigates away that terminates the app, like a process.exit. So the + // dispose functionality here is basically so that it can be used in unit tests. + // + // Based on the APIs that exist in Blazor today it's not possible for the + // app to get disposed, however if we add something like that in the future, most of the work is + // already done. + private bool _disposed; + private bool _started; private WebAssemblyRenderer _renderer; - public WebAssemblyHost(IServiceProvider services, IJSRuntime runtime) + internal WebAssemblyHost(IServiceProvider services, IServiceScope scope, IConfiguration configuration, RootComponentMapping[] rootComponents) { // To ensure JS-invoked methods don't get linked out, have a reference to their enclosing types GC.KeepAlive(typeof(EntrypointInvoker)); GC.KeepAlive(typeof(JSInteropMethods)); GC.KeepAlive(typeof(WebAssemblyEventDispatcher)); - Services = services ?? throw new ArgumentNullException(nameof(services)); - _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + _services = services; + _scope = scope; + _configuration = configuration; + _rootComponents = rootComponents; } - public IServiceProvider Services { get; } + /// + /// Gets the application configuration. + /// + public IConfiguration Configuration => _configuration; - public Task StartAsync(CancellationToken cancellationToken = default) + /// + /// Gets the service provider associated with the application. + /// + public IServiceProvider Services => _scope.ServiceProvider; + + /// + /// Disposes the host asynchronously. + /// + /// A which respresents the completion of disposal. + public async ValueTask DisposeAsync() { - return StartAsyncAwaited(); - } - - private async Task StartAsyncAwaited() - { - var scopeFactory = Services.GetRequiredService(); - _scope = scopeFactory.CreateScope(); - - try + if (_disposed) { - var startup = _scope.ServiceProvider.GetService(); - if (startup == null) + return; + } + + _disposed = true; + + _renderer?.Dispose(); + + if (_scope is IAsyncDisposable asyncDisposableScope) + { + await asyncDisposableScope.DisposeAsync(); + } + else + { + _scope?.Dispose(); + } + + if (_services is IAsyncDisposable asyncDisposableServices) + { + await asyncDisposableServices.DisposeAsync(); + } + else if (_services is IDisposable disposableServices) + { + disposableServices.Dispose(); + } + } + + /// + /// Runs the application associated with this host. + /// + /// A which represents exit of the application. + /// + /// At this time, it's not possible to shut down a Blazor WebAssembly application using imperative code. + /// The application only stops when the hosting page is reloaded or navigated to another page. As a result + /// the task returned from this method does not complete. This method is not suitable for use in unit-testing. + /// + public Task RunAsync() + { + // RunAsyncCore will await until the CancellationToken fires. However, we don't fire it + // currently, so the app will "run" forever. + return RunAsyncCore(CancellationToken.None); + } + + // Internal for testing. + internal async Task RunAsyncCore(CancellationToken cancellationToken) + { + if (_started) + { + throw new InvalidOperationException("The host has already started."); + } + + _started = true; + + var tcs = new TaskCompletionSource(); + + using (cancellationToken.Register(() => { tcs.TrySetResult(null); })) + { + var loggerFactory = Services.GetRequiredService(); + _renderer = new WebAssemblyRenderer(Services, loggerFactory); + + var rootComponents = _rootComponents; + for (var i = 0; i < rootComponents.Length; i++) { - var message = - $"Could not find a registered Blazor Startup class. " + - $"Using {nameof(IWebAssemblyHost)} requires a call to {nameof(IWebAssemblyHostBuilder)}.UseBlazorStartup."; - throw new InvalidOperationException(message); + var rootComponent = rootComponents[i]; + await _renderer.AddComponentAsync(rootComponent.ComponentType, rootComponent.Selector); } - // Note that we differ from the WebHost startup path here by using a 'scope' for the app builder - // as well as the Configure method. - var builder = new WebAssemblyBlazorApplicationBuilder(_scope.ServiceProvider); - startup.Configure(builder, _scope.ServiceProvider); - - _renderer = await builder.CreateRendererAsync(); + await tcs.Task; } - catch - { - _scope.Dispose(); - _scope = null; - - if (_renderer != null) - { - _renderer.Dispose(); - _renderer = null; - } - - throw; - } - } - - public Task StopAsync(CancellationToken cancellationToken = default) - { - if (_scope != null) - { - _scope.Dispose(); - _scope = null; - } - - if (_renderer != null) - { - _renderer.Dispose(); - _renderer = null; - } - - return Task.CompletedTask; - } - - public void Dispose() - { - (Services as IDisposable)?.Dispose(); } } } diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs index 7fa4dd0ae1..1d1f05dce7 100644 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs +++ b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilder.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using Microsoft.AspNetCore.Blazor.Services; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -14,87 +16,89 @@ using Microsoft.JSInterop; namespace Microsoft.AspNetCore.Blazor.Hosting { - // - // This code was taken virtually as-is from the Microsoft.Extensions.Hosting project in aspnet/Hosting and then - // lots of things were removed. - // - internal class WebAssemblyHostBuilder : IWebAssemblyHostBuilder + /// + /// A builder for configuring and creating a . + /// + public sealed class WebAssemblyHostBuilder { - private List> _configureServicesActions = new List>(); - private bool _hostBuilt; - private WebAssemblyHostBuilderContext _BrowserHostBuilderContext; - private IWebAssemblyServiceFactoryAdapter _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(new DefaultServiceProviderFactory()); - private IServiceProvider _appServices; - /// - /// A central location for sharing state between components during the host building process. + /// Creates an instance of using the most common + /// conventions and settings. /// - public IDictionary Properties { get; } = new Dictionary(); - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder ConfigureServices(Action configureDelegate) + /// The argument passed to the application's main method. + /// A . + public static WebAssemblyHostBuilder CreateDefault(string[] args = default) { - _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); - return this; + // We don't use the args for anything right now, but we want to accept them + // here so that it shows up this way in the project templates. + args ??= Array.Empty(); + var builder = new WebAssemblyHostBuilder(); + + // Right now we don't have conventions or behaviors that are specific to this method + // however, making this the default for the template allows us to add things like that + // in the future, while giving `new WebAssemblyHostBuilder` as an opt-out of opinionated + // settings. + return builder; } /// - /// Overrides the factory used to create the service provider. + /// Creates an instance of with the minimal configuration. /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) + private WebAssemblyHostBuilder() { - _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(factory ?? throw new ArgumentNullException(nameof(factory))); - return this; + // Private right now because we don't have much reason to expose it. This can be exposed + // in the future if we want to give people a choice between CreateDefault and something + // less opinionated. + Configuration = new ConfigurationBuilder(); + RootComponents = new RootComponentMappingCollection(); + Services = new ServiceCollection(); + + InitializeDefaultServices(); } /// - /// Overrides the factory used to create the service provider. + /// Gets an that can be used to customize the application's + /// configuration sources. /// - /// The same instance of the for chaining. - public IWebAssemblyHostBuilder UseServiceProviderFactory(Func> factory) - { - _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter(() => _BrowserHostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory))); - return this; - } + public IConfigurationBuilder Configuration { get; } /// - /// Run the given actions to initialize the host. This can only be called once. + /// Gets the collection of root component mappings configured for the application. /// - /// An initialized - public IWebAssemblyHost Build() + public RootComponentMappingCollection RootComponents { get; } + + /// + /// Gets the service collection. + /// + public IServiceCollection Services { get; } + + /// + /// Builds a instance based on the configuration of this builder. + /// + /// A object. + public WebAssemblyHost Build() { - if (_hostBuilt) - { - throw new InvalidOperationException("Build can only be called once."); - } - _hostBuilt = true; + // Intentionally overwrite configuration with the one we're creating. + var configuration = Configuration.Build(); + Services.AddSingleton(configuration); - CreateBrowserHostBuilderContext(); - CreateServiceProvider(); + // A Blazor application always runs in a scope. Since we want to make it possible for the user + // to configure services inside *that scope* inside their startup code, we create *both* the + // service provider and the scope here. + var services = Services.BuildServiceProvider(); + var scope = services.GetRequiredService().CreateScope(); - return _appServices.GetRequiredService(); + return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray()); } - private void CreateBrowserHostBuilderContext() + private void InitializeDefaultServices() { - _BrowserHostBuilderContext = new WebAssemblyHostBuilderContext(Properties); - } - - private void CreateServiceProvider() - { - var services = new ServiceCollection(); - services.AddSingleton(_BrowserHostBuilderContext); - services.AddSingleton(); - services.AddSingleton(WebAssemblyJSRuntime.Instance); - services.AddSingleton(WebAssemblyNavigationManager.Instance); - services.AddSingleton(WebAssemblyNavigationInterception.Instance); - services.AddSingleton(); - services.AddSingleton(s => + Services.AddSingleton(WebAssemblyJSRuntime.Instance); + Services.AddSingleton(WebAssemblyNavigationManager.Instance); + Services.AddSingleton(WebAssemblyNavigationInterception.Instance); + Services.AddSingleton(); + Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>))); + Services.AddSingleton(s => { // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it. var navigationManager = s.GetRequiredService(); @@ -103,20 +107,6 @@ namespace Microsoft.AspNetCore.Blazor.Hosting BaseAddress = new Uri(navigationManager.BaseUri) }; }); - - // Needed for authorization - // However, since authorization isn't on by default, we could consider removing these and - // having a separate services.AddBlazorAuthorization() call that brings in the required services. - services.AddOptions(); - services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>))); - - foreach (var configureServicesAction in _configureServicesActions) - { - configureServicesAction(_BrowserHostBuilderContext, services); - } - - var builder = _serviceProviderFactory.CreateBuilder(services); - _appServices = _serviceProviderFactory.CreateServiceProvider(builder); } } } diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs deleted file mode 100644 index c7b7dd6f19..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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.Collections.Generic; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// Context containing the common services on the . Some properties may be null until set by the . - /// - public sealed class WebAssemblyHostBuilderContext - { - /// - /// Creates a new . - /// - /// The property collection. - public WebAssemblyHostBuilderContext(IDictionary properties) - { - Properties = properties ?? throw new System.ArgumentNullException(nameof(properties)); - } - - /// - /// A central location for sharing state between components during the host building process. - /// - public IDictionary Properties { get; } - } -} \ No newline at end of file diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs deleted file mode 100644 index 9b03c09766..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostBuilderExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// Provides Blazor-specific support for . - /// - public static class WebAssemblyHostBuilderExtensions - { - private const string BlazorStartupKey = "Blazor.Startup"; - - /// - /// Adds services to the container. This can be called multiple times and the results will be additive. - /// - /// The to configure. - /// - /// The same instance of the for chaining. - public static IWebAssemblyHostBuilder ConfigureServices(this IWebAssemblyHostBuilder hostBuilder, Action configureDelegate) - { - return hostBuilder.ConfigureServices((context, collection) => configureDelegate(collection)); - } - - /// - /// Configures the to use the provided startup class. - /// - /// The . - /// A type that configures a Blazor application. - /// The . - public static IWebAssemblyHostBuilder UseBlazorStartup(this IWebAssemblyHostBuilder builder, Type startupType) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (builder.Properties.ContainsKey(BlazorStartupKey)) - { - throw new InvalidOperationException("A startup class has already been registered."); - } - - // It would complicate the implementation to allow multiple startup classes, and we don't - // really have a need for it. - builder.Properties.Add(BlazorStartupKey, bool.TrueString); - - var startup = new ConventionBasedStartup(Activator.CreateInstance(startupType)); - - builder.ConfigureServices(startup.ConfigureServices); - builder.ConfigureServices(s => s.AddSingleton(startup)); - - return builder; - } - - /// - /// Configures the to use the provided startup class. - /// - /// A type that configures a Blazor application. - /// The . - /// The . - public static IWebAssemblyHostBuilder UseBlazorStartup(this IWebAssemblyHostBuilder builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - return UseBlazorStartup(builder, typeof(TStartup)); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs deleted file mode 100644 index d08162a590..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyHostExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - /// - /// Extension methods for . - /// - public static class WebAssemblyHostExtensions - { - /// - /// Runs the application. - /// - /// The to run. - /// - /// Currently, Blazor applications running in the browser don't have a lifecycle - the application does not - /// get a chance to gracefully shut down. For now, simply starts the host - /// and allows execution to continue. - /// - public static void Run(this IWebAssemblyHost host) - { - // Behave like async void, because we don't yet support async-main properly on WebAssembly. - // However, don't actualy make this method async, because we rely on startup being synchronous - // for things like attaching navigation event handlers. - host.StartAsync().ContinueWith(task => - { - if (task.Exception != null) - { - Console.WriteLine(task.Exception); - } - }); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs b/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs deleted file mode 100644 index fcc879653a..0000000000 --- a/src/Components/Blazor/Blazor/src/Hosting/WebAssemblyServiceFactoryAdapter.cs +++ /dev/null @@ -1,52 +0,0 @@ -// 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 Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.Blazor.Hosting -{ - // Equivalent to https://github.com/aspnet/Extensions/blob/master/src/Hosting/Hosting/src/Internal/ServiceFactoryAdapter.cs - - internal class WebAssemblyServiceFactoryAdapter : IWebAssemblyServiceFactoryAdapter - { - private IServiceProviderFactory _serviceProviderFactory; - private readonly Func _contextResolver; - private Func> _factoryResolver; - - public WebAssemblyServiceFactoryAdapter(IServiceProviderFactory serviceProviderFactory) - { - _serviceProviderFactory = serviceProviderFactory ?? throw new ArgumentNullException(nameof(serviceProviderFactory)); - } - - public WebAssemblyServiceFactoryAdapter(Func contextResolver, Func> factoryResolver) - { - _contextResolver = contextResolver ?? throw new ArgumentNullException(nameof(contextResolver)); - _factoryResolver = factoryResolver ?? throw new ArgumentNullException(nameof(factoryResolver)); - } - - public object CreateBuilder(IServiceCollection services) - { - if (_serviceProviderFactory == null) - { - _serviceProviderFactory = _factoryResolver(_contextResolver()); - - if (_serviceProviderFactory == null) - { - throw new InvalidOperationException("The resolver returned a null IServiceProviderFactory"); - } - } - return _serviceProviderFactory.CreateBuilder(services); - } - - public IServiceProvider CreateServiceProvider(object containerBuilder) - { - if (_serviceProviderFactory == null) - { - throw new InvalidOperationException("CreateBuilder must be called before CreateServiceProvider"); - } - - return _serviceProviderFactory.CreateServiceProvider((TContainerBuilder)containerBuilder); - } - } -} diff --git a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj index be94e420a2..ff30d9d9c8 100644 --- a/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj +++ b/src/Components/Blazor/Blazor/src/Microsoft.AspNetCore.Blazor.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -9,7 +9,7 @@ - + diff --git a/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs b/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs deleted file mode 100644 index cbc73b79f8..0000000000 --- a/src/Components/Blazor/Blazor/test/Hosting/ConventionBasedStartupTest.cs +++ /dev/null @@ -1,210 +0,0 @@ -// 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.Blazor.Hosting; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; -using Xunit; - -namespace Microsoft.AspNetCore.Components.Hosting -{ - public class ConventionBasedStartupTest - { - [Fact] - public void ConventionBasedStartup_GetConfigureServicesMethod_FindsConfigureServices() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup1()); - - // Act - var method = startup.GetConfigureServicesMethod(); - - // Assert - Assert.Equal(typeof(IServiceCollection), method.GetParameters()[0].ParameterType); - } - - private class MyStartup1 - { - public void ConfigureServices(IServiceCollection services) - { - } - - // Ignored - public void ConfigureServices(DateTime x) - { - - } - - // Ignored - private void ConfigureServices(int x) - { - } - - // Ignored - public static void ConfigureServices(string x) - { - } - } - - [Fact] - public void ConventionBasedStartup_GetConfigureServicesMethod_NoMethodFound() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup2()); - - // Act - var method = startup.GetConfigureServicesMethod(); - - // Assert - Assert.Null(method); - } - - private class MyStartup2 - { - } - - [Fact] - public void ConventionBasedStartup_ConfigureServices_CallsMethod() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup3()); - var services = new ServiceCollection(); - - // Act - startup.ConfigureServices(services); - - // Assert - Assert.NotEmpty(services); - } - - private class MyStartup3 - { - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton("foo"); - } - } - - [Fact] - public void ConventionBasedStartup_ConfigureServices_NoMethodFound() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup4()); - var services = new ServiceCollection(); - - // Act - startup.ConfigureServices(services); - - // Assert - Assert.Empty(services); - } - - private class MyStartup4 - { - } - - [Fact] - public void ConventionBasedStartup_GetConfigureMethod_FindsConfigure() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup5()); - - // Act - var method = startup.GetConfigureMethod(); - - // Assert - Assert.Empty(method.GetParameters()); - } - - private class MyStartup5 - { - public void Configure() - { - } - - // Ignored - private void Configure(int x) - { - } - - // Ignored - public static void Configure(string x) - { - } - } - - [Fact] - public void ConventionBasedStartup_GetConfigureMethod_NoMethodFoundThrows() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup6()); - - // Act - var ex = Assert.Throws(() => startup.GetConfigureMethod()); - - // Assert - Assert.Equal("The startup class must define a 'Configure' method.", ex.Message); - } - - private class MyStartup6 - { - } - - [Fact] - public void ConventionBasedStartup_GetConfigureMethod_OverloadedThrows() - { - // Arrange - var startup = new ConventionBasedStartup(new MyStartup7()); - - // Act - var ex = Assert.Throws(() => startup.GetConfigureMethod()); - - // Assert - Assert.Equal("Overloading the 'Configure' method is not supported.", ex.Message); - } - - private class MyStartup7 - { - public void Configure() - { - } - - public void Configure(string x) - { - } - } - - [Fact] - public void ConventionBasedStartup_Configure() - { - // Arrange - var instance = new MyStartup8(); - var startup = new ConventionBasedStartup(instance); - - var services = new ServiceCollection().AddSingleton("foo").BuildServiceProvider(); - var builder = new WebAssemblyBlazorApplicationBuilder(services); - - // Act - startup.Configure(builder, services); - - // Assert - Assert.Collection( - instance.Arguments, - a => Assert.Same(builder, a), - a => Assert.Equal("foo", a)); - } - - private class MyStartup8 - { - public List Arguments { get; } = new List(); - - public void Configure(IComponentsApplicationBuilder app, string foo) - { - Arguments.Add(app); - Arguments.Add(foo); - } - } - } -} diff --git a/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs b/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs new file mode 100644 index 0000000000..7249402880 --- /dev/null +++ b/src/Components/Blazor/Blazor/test/Hosting/RootComponentMappingTest.cs @@ -0,0 +1,37 @@ +// 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.Text; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Testing; +using Xunit; + +namespace Microsoft.AspNetCore.Blazor.Hosting +{ + public class RootComponentMappingTest + { + [Fact] + public void Constructor_ValidatesComponentType_Success() + { + // Arrange + // Act + var mapping = new RootComponentMapping(typeof(Router), "test"); + + // Assert (does not throw) + GC.KeepAlive(mapping); + } + + [Fact] + public void Constructor_ValidatesComponentType_Failure() + { + // Arrange + // Act & Assert + ExceptionAssert.ThrowsArgument( + () => new RootComponentMapping(typeof(StringBuilder), "test"), + "componentType", + $"The type '{nameof(StringBuilder)}' must implement IComponent to be used as a root component."); + } + } +} diff --git a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs index 4acf6f99a3..77b6583d26 100644 --- a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs +++ b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostBuilderTest.cs @@ -3,160 +3,100 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Text; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using Xunit; -namespace Microsoft.AspNetCore.Blazor.Hosting.Test +namespace Microsoft.AspNetCore.Blazor.Hosting { public class WebAssemblyHostBuilderTest { [Fact] - public void HostBuilder_CanCallBuild_BuildsServices() + public void Build_AllowsConfiguringConfiguration() { // Arrange - var builder = new WebAssemblyHostBuilder(); + var builder = WebAssemblyHostBuilder.CreateDefault(); - // Act - var host = builder.Build(); - - // Assert - Assert.NotNull(host.Services.GetService(typeof(IWebAssemblyHost))); - } - - [Fact] - public void HostBuilder_CanConfigureAdditionalServices() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.ConfigureServices((c, s) => s.AddSingleton("foo")); - builder.ConfigureServices((c, s) => s.AddSingleton(new StringBuilder("bar"))); - - // Act - var host = builder.Build(); - - // Assert - Assert.Equal("foo", host.Services.GetService(typeof(string))); - Assert.Equal("bar", host.Services.GetService(typeof(StringBuilder)).ToString()); - } - - [Fact] - public void HostBuilder_UseBlazorStartup_CanConfigureAdditionalServices() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.UseBlazorStartup(); - builder.ConfigureServices((c, s) => s.AddSingleton(new StringBuilder("bar"))); - - // Act - var host = builder.Build(); - - // Assert - Assert.Equal("foo", host.Services.GetService(typeof(string))); - Assert.Equal("bar", host.Services.GetService(typeof(StringBuilder)).ToString()); - } - - [Fact] - public void HostBuilder_UseBlazorStartup_DoesNotAllowMultiple() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.UseBlazorStartup(); - - // Act - var ex = Assert.Throws(() => builder.UseBlazorStartup()); - - // Assert - Assert.Equal("A startup class has already been registered.", ex.Message); - } - - private class MyStartup - { - public void ConfigureServices(IServiceCollection services) + builder.Configuration.AddInMemoryCollection(new[] { - services.AddSingleton("foo"); - } - } - - [Fact] - public void HostBuilder_CanCustomizeServiceFactory() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.UseServiceProviderFactory(new TestServiceProviderFactory()); - - // Act - var host = builder.Build(); - - // Assert - Assert.IsType(host.Services); - } - - [Fact] - public void HostBuilder_CanCustomizeServiceFactoryWithContext() - { - // Arrange - var builder = new WebAssemblyHostBuilder(); - builder.UseServiceProviderFactory(context => - { - Assert.NotNull(context.Properties); - Assert.Same(builder.Properties, context.Properties); - return new TestServiceProviderFactory(); + new KeyValuePair("key", "value"), }); // Act var host = builder.Build(); // Assert - Assert.IsType(host.Services); + Assert.Equal("value", host.Configuration["key"]); } - private class TestServiceProvider : IServiceProvider + [Fact] + public void Build_AllowsConfiguringServices() { - private readonly IServiceProvider _underlyingProvider; + // Arrange + var builder = WebAssemblyHostBuilder.CreateDefault(); - public TestServiceProvider(IServiceProvider underlyingProvider) - { - _underlyingProvider = underlyingProvider; - } + // This test also verifies that we create a scope. + builder.Services.AddScoped(); - public object GetService(Type serviceType) + // Act + var host = builder.Build(); + + // Assert + Assert.NotNull(host.Services.GetRequiredService()); + } + + [Fact] + public void Build_AddsConfigurationToServices() + { + // Arrange + var builder = WebAssemblyHostBuilder.CreateDefault(); + + builder.Configuration.AddInMemoryCollection(new[] { - if (serviceType == typeof(IWebAssemblyHost)) + new KeyValuePair("key", "value"), + }); + + // Act + var host = builder.Build(); + + // Assert + var configuration = host.Services.GetRequiredService(); + Assert.Equal("value", configuration["key"]); + } + + private static IReadOnlyList DefaultServiceTypes + { + get + { + return new Type[] { - // Since the test will make assertions about the resulting IWebAssemblyHost, - // show that custom DI containers have the power to substitute themselves - // as the IServiceProvider - return new WebAssemblyHost( - this, _underlyingProvider.GetRequiredService()); - } - else - { - return _underlyingProvider.GetService(serviceType); - } + typeof(IJSRuntime), + typeof(NavigationManager), + typeof(INavigationInterception), + typeof(ILoggerFactory), + typeof(HttpClient), + typeof(ILogger<>), + }; } } - private class TestServiceProviderFactory : IServiceProviderFactory + [Fact] + public void Constructor_AddsDefaultServices() { - public IServiceCollection CreateBuilder(IServiceCollection services) - { - return new TestServiceCollection(services); - } + // Arrange & Act + var builder = WebAssemblyHostBuilder.CreateDefault(); - public IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) + // Assert + Assert.Equal(DefaultServiceTypes.Count, builder.Services.Count); + foreach (var type in DefaultServiceTypes) { - Assert.IsType(serviceCollection); - return new TestServiceProvider(serviceCollection.BuildServiceProvider()); - } - - class TestServiceCollection : List, IServiceCollection - { - public TestServiceCollection(IEnumerable collection) - : base(collection) - { - } + Assert.Single(builder.Services, d => d.ServiceType == type); } } } diff --git a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs index f99245e317..b838334566 100644 --- a/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs +++ b/src/Components/Blazor/Blazor/test/Hosting/WebAssemblyHostTest.cs @@ -2,64 +2,89 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.AspNetCore.Components.Hosting; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; -using Microsoft.JSInterop; -using Mono.WebAssembly.Interop; using Xunit; -namespace Microsoft.AspNetCore.Blazor.Hosting.Test +namespace Microsoft.AspNetCore.Blazor.Hosting { public class WebAssemblyHostTest { - [Fact] - public async Task BrowserHost_StartAsync_ThrowsWithoutStartup() + // This won't happen in the product code, but we need to be able to safely call RunAsync + // to be able to test a few of the other details. + [Fact] + public async Task RunAsync_CanExitBasedOnCancellationToken() { // Arrange - var builder = new WebAssemblyHostBuilder(); + var builder = WebAssemblyHostBuilder.CreateDefault(); var host = builder.Build(); - // Act - var ex = await Assert.ThrowsAsync(async () => await host.StartAsync()); + var cts = new CancellationTokenSource(); - // Assert - Assert.Equal( - "Could not find a registered Blazor Startup class. " + - "Using IWebAssemblyHost requires a call to IWebAssemblyHostBuilder.UseBlazorStartup.", - ex.Message); + // Act + var task = host.RunAsyncCore(cts.Token); + + cts.Cancel(); + await task.TimeoutAfter(TimeSpan.FromSeconds(3)); + + // Assert (does not throw) } [Fact] - public async Task BrowserHost_StartAsync_RunsConfigureMethod() + public async Task RunAsync_CallingTwiceCausesException() { // Arrange - var builder = new WebAssemblyHostBuilder(); - - var startup = new MockStartup(); - builder.ConfigureServices((c, s) => { s.AddSingleton(startup); }); - + var builder = WebAssemblyHostBuilder.CreateDefault(); var host = builder.Build(); + var cts = new CancellationTokenSource(); + var task = host.RunAsyncCore(cts.Token); + // Act - await host.StartAsync(); + var ex = await Assert.ThrowsAsync(() => host.RunAsyncCore(cts.Token)); + + cts.Cancel(); + await task.TimeoutAfter(TimeSpan.FromSeconds(3)); // Assert - Assert.True(startup.ConfigureCalled); + Assert.Equal("The host has already started.", ex.Message); } - private class MockStartup : IBlazorStartup + [Fact] + public async Task DisposeAsync_CanDisposeAfterCallingRunAsync() { - public bool ConfigureCalled { get; set; } + // Arrange + var builder = WebAssemblyHostBuilder.CreateDefault(); + builder.Services.AddSingleton(); + var host = builder.Build(); - public void Configure(IComponentsApplicationBuilder app, IServiceProvider services) + var disposable = host.Services.GetRequiredService(); + + var cts = new CancellationTokenSource(); + + // Act + await using (host) { - ConfigureCalled = true; + var task = host.RunAsyncCore(cts.Token); + + cts.Cancel(); + await task.TimeoutAfter(TimeSpan.FromSeconds(3)); } - public void ConfigureServices(IServiceCollection services) + // Assert + Assert.Equal(1, disposable.DisposeCount); + } + + private class DisposableService : IAsyncDisposable + { + public int DisposeCount { get; private set; } + + public ValueTask DisposeAsync() { + DisposeCount++; + return new ValueTask(Task.CompletedTask); } } } diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs b/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs index 69ee439533..e922c2996f 100644 --- a/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs +++ b/src/Components/Blazor/testassets/HostedInAspNet.Client/Program.cs @@ -1,19 +1,19 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Hosting; namespace HostedInAspNet.Client { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + await builder.Build().RunAsync(); + } } } diff --git a/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs b/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs deleted file mode 100644 index a06163b9e0..0000000000 --- a/src/Components/Blazor/testassets/HostedInAspNet.Client/Startup.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace HostedInAspNet.Client -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/Blazor/testassets/StandaloneApp/Program.cs b/src/Components/Blazor/testassets/StandaloneApp/Program.cs index 530de72870..8da14834b6 100644 --- a/src/Components/Blazor/testassets/StandaloneApp/Program.cs +++ b/src/Components/Blazor/testassets/StandaloneApp/Program.cs @@ -1,19 +1,19 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Hosting; namespace StandaloneApp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + await builder.Build().RunAsync(); + } } } diff --git a/src/Components/Blazor/testassets/StandaloneApp/Startup.cs b/src/Components/Blazor/testassets/StandaloneApp/Startup.cs deleted file mode 100644 index 79564e8d9d..0000000000 --- a/src/Components/Blazor/testassets/StandaloneApp/Startup.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace StandaloneApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs b/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs index 0d74c8c6b7..6a721adf63 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs +++ b/src/Components/benchmarkapps/Wasm.Performance/Driver/Program.cs @@ -100,7 +100,7 @@ namespace Wasm.Performance.Driver Format = "n2", }); - var testAssembly = typeof(TestApp.Startup).Assembly; + var testAssembly = typeof(TestApp.Program).Assembly; var testAssemblyLocation = new FileInfo(testAssembly.Location); var testApp = new DirectoryInfo(Path.Combine( testAssemblyLocation.Directory.FullName, @@ -143,7 +143,7 @@ namespace Wasm.Performance.Driver var args = new[] { "--urls", "http://127.0.0.1:0", - "--applicationpath", typeof(TestApp.Startup).Assembly.Location, + "--applicationpath", typeof(TestApp.Program).Assembly.Location, }; var host = DevHostServerProgram.BuildWebHost(args); diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs index 403bc37c9c..57a3697382 100644 --- a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs +++ b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Program.cs @@ -1,19 +1,19 @@ // 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.Threading.Tasks; using Microsoft.AspNetCore.Blazor.Hosting; namespace Wasm.Performance.TestApp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + await builder.Build().RunAsync(); + } } } diff --git a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs b/src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs deleted file mode 100644 index c79b0efb8c..0000000000 --- a/src/Components/benchmarkapps/Wasm.Performance/TestApp/Startup.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace Wasm.Performance.TestApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/Components/test/testassets/BasicTestApp/Program.cs b/src/Components/test/testassets/BasicTestApp/Program.cs index 2be7d81b4e..0c62f05bd1 100644 --- a/src/Components/test/testassets/BasicTestApp/Program.cs +++ b/src/Components/test/testassets/BasicTestApp/Program.cs @@ -3,8 +3,13 @@ using System; using System.Globalization; +using System.Runtime.InteropServices; using System.Threading.Tasks; +using BasicTestApp.AuthTest; using Microsoft.AspNetCore.Blazor.Hosting; +using Microsoft.AspNetCore.Blazor.Http; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; using Mono.WebAssembly.Interop; namespace BasicTestApp @@ -18,12 +23,26 @@ namespace BasicTestApp // We want the culture to be en-US so that the tests for bind can work consistently. CultureInfo.CurrentCulture = new CultureInfo("en-US"); - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"))) + { + // Needed because the test server runs on a different port than the client app, + // and we want to test sending/receiving cookies under this config + WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include; + } + + builder.RootComponents.Add("root"); + + builder.Services.AddSingleton(); + builder.Services.AddAuthorizationCore(options => + { + options.AddPolicy("NameMustStartWithB", policy => + policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); + }); + + await builder.Build().RunAsync(); + } // Supports E2E tests in StartupErrorNotificationTest private static async Task SimulateErrorsIfNeededForTest() diff --git a/src/Components/test/testassets/BasicTestApp/Startup.cs b/src/Components/test/testassets/BasicTestApp/Startup.cs deleted file mode 100644 index 008a988316..0000000000 --- a/src/Components/test/testassets/BasicTestApp/Startup.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Runtime.InteropServices; -using BasicTestApp.AuthTest; -using Microsoft.AspNetCore.Blazor.Http; -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Authorization; -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -namespace BasicTestApp -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddSingleton(); - - services.AddAuthorizationCore(options => - { - options.AddPolicy("NameMustStartWithB", policy => - policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false)); - }); - } - - public void Configure(IComponentsApplicationBuilder app) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"))) - { - // Needed because the test server runs on a different port than the client app, - // and we want to test sending/receiving cookies underling this config - WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include; - } - - app.AddComponent("root"); - } - } -} diff --git a/src/Components/test/testassets/TestServer/CorsStartup.cs b/src/Components/test/testassets/TestServer/CorsStartup.cs index a4d91a84aa..28dda32d90 100644 --- a/src/Components/test/testassets/TestServer/CorsStartup.cs +++ b/src/Components/test/testassets/TestServer/CorsStartup.cs @@ -46,7 +46,7 @@ namespace TestServer app.Map("/subdir", app => { app.UseStaticFiles(); - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); app.UseRouting(); @@ -55,7 +55,7 @@ namespace TestServer app.UseEndpoints(endpoints => { endpoints.MapControllers(); - endpoints.MapFallbackToClientSideBlazor("index.html"); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); }); } diff --git a/src/Components/test/testassets/TestServer/InternationalizationStartup.cs b/src/Components/test/testassets/TestServer/InternationalizationStartup.cs index d508ed797b..7521ebe34b 100644 --- a/src/Components/test/testassets/TestServer/InternationalizationStartup.cs +++ b/src/Components/test/testassets/TestServer/InternationalizationStartup.cs @@ -37,7 +37,7 @@ namespace TestServer app.Map("/subdir", app => { app.UseStaticFiles(); - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); app.UseRequestLocalization(options => { diff --git a/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs b/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs index 483dd00d78..d32f48f8e3 100644 --- a/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs +++ b/src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs @@ -36,7 +36,7 @@ namespace TestServer // The client-side files middleware needs to be here because the base href in hardcoded to /subdir/ app.Map("/subdir", app => { - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); }); // The calls to `Map` allow us to test each of these overloads, while keeping them isolated. @@ -46,7 +46,7 @@ namespace TestServer app.UseEndpoints(endpoints => { - endpoints.MapFallbackToClientSideBlazor("index.html"); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); }); @@ -56,7 +56,7 @@ namespace TestServer app.UseEndpoints(endpoints => { - endpoints.MapFallbackToClientSideBlazor("test/{*path:nonfile}", "index.html"); + endpoints.MapFallbackToClientSideBlazor("test/{*path:nonfile}", "index.html"); }); }); @@ -66,7 +66,7 @@ namespace TestServer app.UseEndpoints(endpoints => { - endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Startup).Assembly.Location, "index.html"); + endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Program).Assembly.Location, "index.html"); }); }); @@ -76,7 +76,7 @@ namespace TestServer app.UseEndpoints(endpoints => { - endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Startup).Assembly.Location, "test/{*path:nonfile}", "index.html"); + endpoints.MapFallbackToClientSideBlazor(typeof(BasicTestApp.Program).Assembly.Location, "test/{*path:nonfile}", "index.html"); }); }); } diff --git a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Program.cs b/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Program.cs index 03d510452d..06790b9c57 100644 --- a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Program.cs +++ b/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Program.cs @@ -1,4 +1,12 @@ -using Microsoft.AspNetCore.Blazor.Hosting; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Text; +using Microsoft.AspNetCore.Blazor.Hosting; +using Microsoft.Extensions.DependencyInjection; +#if (!NoAuth) +using Microsoft.AspNetCore.Components.Authorization; +#endif #if (Hosted) namespace BlazorWasm_CSharp.Client @@ -8,13 +16,19 @@ namespace BlazorWasm_CSharp { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebAssemblyHostBuilder.CreateDefault(args); + builder.RootComponents.Add("app"); - public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) => - BlazorWebAssemblyHost.CreateDefaultBuilder() - .UseBlazorStartup(); + // use builder.Services to configure application services. +#if (IndividualLocalAuth) + builder.Services.AddOptions(); + builder.Services.AddAuthorizationCore(); + builder.Services.AddSingleton(); +#endif + + await builder.Build().RunAsync(); + } } } diff --git a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Startup.cs b/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Startup.cs deleted file mode 100644 index 38fd10a30e..0000000000 --- a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Client/Startup.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if (!NoAuth) -using Microsoft.AspNetCore.Components.Authorization; -#endif -using Microsoft.AspNetCore.Components.Builder; -using Microsoft.Extensions.DependencyInjection; - -#if (Hosted) -namespace BlazorWasm_CSharp.Client -#else -namespace BlazorWasm_CSharp -#endif -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { -#if (IndividualLocalAuth) - services.AddAuthorizationCore(); - services.AddSingleton(); -#endif - } - - public void Configure(IComponentsApplicationBuilder app) - { - app.AddComponent("app"); - } - } -} diff --git a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Server/Startup.cs b/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Server/Startup.cs index fd1767bd47..eb96bae0e8 100644 --- a/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Server/Startup.cs +++ b/src/ProjectTemplates/BlazorWasm.ProjectTemplates/content/BlazorWasm-CSharp/Server/Startup.cs @@ -83,7 +83,7 @@ namespace BlazorWasm_CSharp.Server #endif app.UseStaticFiles(); - app.UseClientSideBlazorFiles(); + app.UseClientSideBlazorFiles(); app.UseRouting(); @@ -99,7 +99,7 @@ namespace BlazorWasm_CSharp.Server #endif endpoints.MapControllers(); - endpoints.MapFallbackToClientSideBlazor("index.html"); + endpoints.MapFallbackToClientSideBlazor("index.html"); }); } }