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.
This commit is contained in:
Ryan Nowak 2020-01-19 15:51:50 -08:00
parent ba08d60cb9
commit 3111032dc0
39 changed files with 545 additions and 1249 deletions

View File

@ -7,6 +7,6 @@
<Compile Include="Microsoft.AspNetCore.Blazor.netstandard2.1.cs" />
<Reference Include="Mono.WebAssembly.Interop" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
</Project>

View File

@ -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<Microsoft.AspNetCore.Blazor.Hosting.RootComponentMapping>
{
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<Microsoft.AspNetCore.Blazor.Hosting.RootComponentMapping> items) { }
public void Add<TComponent>(string selector) where TComponent : Microsoft.AspNetCore.Components.IComponent { }
}
public partial interface IWebAssemblyHostBuilder
public sealed partial class WebAssemblyHost : System.IAsyncDisposable
{
System.Collections.Generic.IDictionary<object, object> Properties { get; }
Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHost Build();
Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder ConfigureServices(System.Action<Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHostBuilderContext, Microsoft.Extensions.DependencyInjection.IServiceCollection> configureDelegate);
Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<TContainerBuilder> factory);
Microsoft.AspNetCore.Blazor.Hosting.IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(System.Func<Microsoft.AspNetCore.Blazor.Hosting.WebAssemblyHostBuilderContext, Microsoft.Extensions.DependencyInjection.IServiceProviderFactory<TContainerBuilder>> 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<object, object> properties) { }
public System.Collections.Generic.IDictionary<object, object> 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<Microsoft.Extensions.DependencyInjection.IServiceCollection> 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<TStartup>(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<TComponent>(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);
}
}

View File

@ -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
{
/// <summary>
/// Provides extension methods for <see cref="IComponentsApplicationBuilder"/>.
/// </summary>
public static class ComponentsApplicationBuilderExtensions
{
/// <summary>
/// Associates the component type with the application,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="app">The <see cref="IComponentsApplicationBuilder"/>.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public static void AddComponent<TComponent>(this IComponentsApplicationBuilder app, string domElementSelector)
where TComponent : IComponent
{
app.AddComponent(typeof(TComponent), domElementSelector);
}
}
}

View File

@ -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
{
/// <summary>
/// A builder for adding components to an application.
/// </summary>
public interface IComponentsApplicationBuilder
{
/// <summary>
/// Gets the application services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Associates the <see cref="IComponent"/> with the application,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
void AddComponent(Type componentType, string domElementSelector);
}
}

View File

@ -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
{
/// <summary>
/// Used to create an instance of Blazor host builder for a Browser application.
/// </summary>
public static class BlazorWebAssemblyHost
{
/// <summary>
/// Creates an instance of <see cref="IWebAssemblyHostBuilder"/>.
/// </summary>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
public static IWebAssemblyHostBuilder CreateDefaultBuilder()
{
return new WebAssemblyHostBuilder();
}
}
}

View File

@ -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<ParameterModifier>());
}
}
}

View File

@ -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);
}
}

View File

@ -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
{
/// <summary>
/// A program abstraction.
/// </summary>
public interface IWebAssemblyHost : IDisposable
{
/// <summary>
/// The programs configured services.
/// </summary>
IServiceProvider Services { get; }
/// <summary>
/// Start the program.
/// </summary>
/// <param name="cancellationToken">Used to abort program start.</param>
/// <returns></returns>
Task StartAsync(CancellationToken cancellationToken = default);
/// <summary>
/// Attempts to gracefully stop the program.
/// </summary>
/// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
/// <returns></returns>
Task StopAsync(CancellationToken cancellationToken = default);
}
}

View File

@ -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
{
/// <summary>
/// Abstraction for configuring a Blazor browser-based application.
/// </summary>
public interface IWebAssemblyHostBuilder
{
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
IDictionary<object, object> Properties { get; }
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory);
/// <summary>
/// Overrides the factory used to create the service provider.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory);
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate">The delegate for configuring the <see cref="IServiceCollection"/> that will be used
/// to construct the <see cref="IServiceProvider"/>.</param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
IWebAssemblyHostBuilder ConfigureServices(Action<WebAssemblyHostBuilderContext, IServiceCollection> configureDelegate);
/// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IWebAssemblyHost"/></returns>
IWebAssemblyHost Build();
}
}

View File

@ -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);
}
}

View File

@ -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
{
/// <summary>
/// Defines a mapping between a root <see cref="IComponent"/> and a DOM element selector.
/// </summary>
public readonly struct RootComponentMapping
{
/// <summary>
/// Creates a new instance of <see cref="RootComponentMapping"/> with the provided <paramref name="componentType"/>
/// and <paramref name="selector"/>.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
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;
}
/// <summary>
/// Gets the component type.
/// </summary>
public Type ComponentType { get; }
/// <summary>
/// Gets the DOM element selector.
/// </summary>
public string Selector { get; }
}
}

View File

@ -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
{
/// <summary>
/// Defines a collection of <see cref="RootComponentMapping"/> items.
/// </summary>
public class RootComponentMappingCollection : Collection<RootComponentMapping>
{
/// <summary>
/// Adds a component mapping to the collection.
/// </summary>
/// <typeparam name="TComponent">The component type.</typeparam>
/// <param name="selector">The DOM element selector.</param>
public void Add<TComponent>(string selector) where TComponent : IComponent
{
if (selector is null)
{
throw new ArgumentNullException(nameof(selector));
}
Add(new RootComponentMapping(typeof(TComponent), selector));
}
/// <summary>
/// Adds a component mapping to the collection.
/// </summary>
/// <param name="componentType">The component type. Must implement <see cref="IComponent"/>.</param>
/// <param name="selector">The DOM element selector.</param>
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));
}
/// <summary>
/// Adds a collection of items to this collection.
/// </summary>
/// <param name="items">The items to add.</param>
public void AddRange(IEnumerable<RootComponentMapping> items)
{
if (items is null)
{
throw new ArgumentNullException(nameof(items));
}
foreach (var item in items)
{
Add(item);
}
}
}
}

View File

@ -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<WebAssemblyRenderer> 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;
}
}
}

View File

@ -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
/// <summary>
/// A host object for Blazor running under WebAssembly. Use <see cref="WebAssemblyHostBuilder"/>
/// to initialize a <see cref="WebAssemblyHost"/>.
/// </summary>
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; }
/// <summary>
/// Gets the application configuration.
/// </summary>
public IConfiguration Configuration => _configuration;
public Task StartAsync(CancellationToken cancellationToken = default)
/// <summary>
/// Gets the service provider associated with the application.
/// </summary>
public IServiceProvider Services => _scope.ServiceProvider;
/// <summary>
/// Disposes the host asynchronously.
/// </summary>
/// <returns>A <see cref="ValueTask"/> which respresents the completion of disposal.</returns>
public async ValueTask DisposeAsync()
{
return StartAsyncAwaited();
}
private async Task StartAsyncAwaited()
{
var scopeFactory = Services.GetRequiredService<IServiceScopeFactory>();
_scope = scopeFactory.CreateScope();
try
if (_disposed)
{
var startup = _scope.ServiceProvider.GetService<IBlazorStartup>();
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();
}
}
/// <summary>
/// Runs the application associated with this host.
/// </summary>
/// <returns>A <see cref="Task"/> which represents exit of the application.</returns>
/// <remarks>
/// 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.
/// </remarks>
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<object>();
using (cancellationToken.Register(() => { tcs.TrySetResult(null); }))
{
var loggerFactory = Services.GetRequiredService<ILoggerFactory>();
_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();
}
}
}

View File

@ -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
/// <summary>
/// A builder for configuring and creating a <see cref="WebAssemblyHost"/>.
/// </summary>
public sealed class WebAssemblyHostBuilder
{
private List<Action<WebAssemblyHostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<WebAssemblyHostBuilderContext, IServiceCollection>>();
private bool _hostBuilt;
private WebAssemblyHostBuilderContext _BrowserHostBuilderContext;
private IWebAssemblyServiceFactoryAdapter _serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<IServiceCollection>(new DefaultServiceProviderFactory());
private IServiceProvider _appServices;
/// <summary>
/// A central location for sharing state between components during the host building process.
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
/// conventions and settings.
/// </summary>
public IDictionary<object, object> Properties { get; } = new Dictionary<object, object>();
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder ConfigureServices(Action<WebAssemblyHostBuilderContext, IServiceCollection> configureDelegate)
/// <param name="args">The argument passed to the application's main method.</param>
/// <returns>A <see cref="WebAssemblyHostBuilder"/>.</returns>
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<string>();
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;
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> with the minimal configuration.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
private WebAssemblyHostBuilder()
{
_serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<TContainerBuilder>(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();
}
/// <summary>
/// Overrides the factory used to create the service provider.
/// Gets an <see cref="IConfigurationBuilder"/> that can be used to customize the application's
/// configuration sources.
/// </summary>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public IWebAssemblyHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory)
{
_serviceProviderFactory = new WebAssemblyServiceFactoryAdapter<TContainerBuilder>(() => _BrowserHostBuilderContext, factory ?? throw new ArgumentNullException(nameof(factory)));
return this;
}
public IConfigurationBuilder Configuration { get; }
/// <summary>
/// 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.
/// </summary>
/// <returns>An initialized <see cref="IWebAssemblyHost"/></returns>
public IWebAssemblyHost Build()
public RootComponentMappingCollection RootComponents { get; }
/// <summary>
/// Gets the service collection.
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Builds a <see cref="WebAssemblyHost"/> instance based on the configuration of this builder.
/// </summary>
/// <returns>A <see cref="WebAssemblyHost"/> object.</returns>
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<IConfiguration>(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<IServiceScopeFactory>().CreateScope();
return _appServices.GetRequiredService<IWebAssemblyHost>();
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<IWebAssemblyHost, WebAssemblyHost>();
services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
services.AddSingleton<HttpClient>(s =>
Services.AddSingleton<IJSRuntime>(WebAssemblyJSRuntime.Instance);
Services.AddSingleton<NavigationManager>(WebAssemblyNavigationManager.Instance);
Services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
Services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
Services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
Services.AddSingleton<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var navigationManager = s.GetRequiredService<NavigationManager>();
@ -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);
}
}
}

View File

@ -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
{
/// <summary>
/// Context containing the common services on the <see cref="IWebAssemblyHost" />. Some properties may be null until set by the <see cref="IWebAssemblyHost" />.
/// </summary>
public sealed class WebAssemblyHostBuilderContext
{
/// <summary>
/// Creates a new <see cref="WebAssemblyHostBuilderContext" />.
/// </summary>
/// <param name="properties">The property collection.</param>
public WebAssemblyHostBuilderContext(IDictionary<object, object> properties)
{
Properties = properties ?? throw new System.ArgumentNullException(nameof(properties));
}
/// <summary>
/// A central location for sharing state between components during the host building process.
/// </summary>
public IDictionary<object, object> Properties { get; }
}
}

View File

@ -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
{
/// <summary>
/// Provides Blazor-specific support for <see cref="IWebAssemblyHost"/>.
/// </summary>
public static class WebAssemblyHostBuilderExtensions
{
private const string BlazorStartupKey = "Blazor.Startup";
/// <summary>
/// Adds services to the container. This can be called multiple times and the results will be additive.
/// </summary>
/// <param name="hostBuilder">The <see cref="IWebAssemblyHostBuilder" /> to configure.</param>
/// <param name="configureDelegate"></param>
/// <returns>The same instance of the <see cref="IWebAssemblyHostBuilder"/> for chaining.</returns>
public static IWebAssemblyHostBuilder ConfigureServices(this IWebAssemblyHostBuilder hostBuilder, Action<IServiceCollection> configureDelegate)
{
return hostBuilder.ConfigureServices((context, collection) => configureDelegate(collection));
}
/// <summary>
/// Configures the <see cref="IWebAssemblyHostBuilder"/> to use the provided startup class.
/// </summary>
/// <param name="builder">The <see cref="IWebAssemblyHostBuilder"/>.</param>
/// <param name="startupType">A type that configures a Blazor application.</param>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
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<IBlazorStartup>(startup));
return builder;
}
/// <summary>
/// Configures the <see cref="IWebAssemblyHostBuilder"/> to use the provided startup class.
/// </summary>
/// <typeparam name="TStartup">A type that configures a Blazor application.</typeparam>
/// <param name="builder">The <see cref="IWebAssemblyHostBuilder"/>.</param>
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
public static IWebAssemblyHostBuilder UseBlazorStartup<TStartup>(this IWebAssemblyHostBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
return UseBlazorStartup(builder, typeof(TStartup));
}
}
}

View File

@ -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
{
/// <summary>
/// Extension methods for <see cref="IWebAssemblyHost"/>.
/// </summary>
public static class WebAssemblyHostExtensions
{
/// <summary>
/// Runs the application.
/// </summary>
/// <param name="host">The <see cref="IWebAssemblyHost"/> to run.</param>
/// <remarks>
/// 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, <see cref="Run(IWebAssemblyHost)"/> simply starts the host
/// and allows execution to continue.
/// </remarks>
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);
}
});
}
}
}

View File

@ -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<TContainerBuilder> : IWebAssemblyServiceFactoryAdapter
{
private IServiceProviderFactory<TContainerBuilder> _serviceProviderFactory;
private readonly Func<WebAssemblyHostBuilderContext> _contextResolver;
private Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> _factoryResolver;
public WebAssemblyServiceFactoryAdapter(IServiceProviderFactory<TContainerBuilder> serviceProviderFactory)
{
_serviceProviderFactory = serviceProviderFactory ?? throw new ArgumentNullException(nameof(serviceProviderFactory));
}
public WebAssemblyServiceFactoryAdapter(Func<WebAssemblyHostBuilderContext> contextResolver, Func<WebAssemblyHostBuilderContext, IServiceProviderFactory<TContainerBuilder>> 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);
}
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
@ -9,7 +9,7 @@
<ItemGroup>
<Reference Include="Mono.WebAssembly.Interop" />
<Reference Include="Microsoft.AspNetCore.Components.Web" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>

View File

@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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<object> Arguments { get; } = new List<object>();
public void Configure(IComponentsApplicationBuilder app, string foo)
{
Arguments.Add(app);
Arguments.Add(foo);
}
}
}
}

View File

@ -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.");
}
}
}

View File

@ -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<string>("foo"));
builder.ConfigureServices((c, s) => s.AddSingleton<StringBuilder>(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<MyStartup>();
builder.ConfigureServices((c, s) => s.AddSingleton<StringBuilder>(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<MyStartup>();
// Act
var ex = Assert.Throws<InvalidOperationException>(() => builder.UseBlazorStartup<MyStartup>());
// 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<string>("foo");
}
}
[Fact]
public void HostBuilder_CanCustomizeServiceFactory()
{
// Arrange
var builder = new WebAssemblyHostBuilder();
builder.UseServiceProviderFactory(new TestServiceProviderFactory());
// Act
var host = builder.Build();
// Assert
Assert.IsType<TestServiceProvider>(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<string, string>("key", "value"),
});
// Act
var host = builder.Build();
// Assert
Assert.IsType<TestServiceProvider>(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<StringBuilder>();
public object GetService(Type serviceType)
// Act
var host = builder.Build();
// Assert
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
}
[Fact]
public void Build_AddsConfigurationToServices()
{
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
builder.Configuration.AddInMemoryCollection(new[]
{
if (serviceType == typeof(IWebAssemblyHost))
new KeyValuePair<string, string>("key", "value"),
});
// Act
var host = builder.Build();
// Assert
var configuration = host.Services.GetRequiredService<IConfiguration>();
Assert.Equal("value", configuration["key"]);
}
private static IReadOnlyList<Type> 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<IJSRuntime>());
}
else
{
return _underlyingProvider.GetService(serviceType);
}
typeof(IJSRuntime),
typeof(NavigationManager),
typeof(INavigationInterception),
typeof(ILoggerFactory),
typeof(HttpClient),
typeof(ILogger<>),
};
}
}
private class TestServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
[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<TestServiceCollection>(serviceCollection);
return new TestServiceProvider(serviceCollection.BuildServiceProvider());
}
class TestServiceCollection : List<ServiceDescriptor>, IServiceCollection
{
public TestServiceCollection(IEnumerable<ServiceDescriptor> collection)
: base(collection)
{
}
Assert.Single(builder.Services, d => d.ServiceType == type);
}
}
}

View File

@ -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<InvalidOperationException>(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<IBlazorStartup>(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<InvalidOperationException>(() => 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<DisposableService>();
var host = builder.Build();
public void Configure(IComponentsApplicationBuilder app, IServiceProvider services)
var disposable = host.Services.GetRequiredService<DisposableService>();
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);
}
}
}

View File

@ -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<Home>("app");
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
await builder.Build().RunAsync();
}
}
}

View File

@ -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<Home>("app");
}
}
}

View File

@ -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>("app");
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
await builder.Build().RunAsync();
}
}
}

View File

@ -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>("app");
}
}
}

View File

@ -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);

View File

@ -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>("app");
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
await builder.Build().RunAsync();
}
}
}

View File

@ -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>("app");
}
}
}

View File

@ -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<Startup>();
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<Index>("root");
builder.Services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
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()

View File

@ -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<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
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<Index>("root");
}
}
}

View File

@ -46,7 +46,7 @@ namespace TestServer
app.Map("/subdir", app =>
{
app.UseStaticFiles();
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
app.UseClientSideBlazorFiles<BasicTestApp.Program>();
app.UseRouting();
@ -55,7 +55,7 @@ namespace TestServer
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("index.html");
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Program>("index.html");
});
});
}

View File

@ -37,7 +37,7 @@ namespace TestServer
app.Map("/subdir", app =>
{
app.UseStaticFiles();
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
app.UseClientSideBlazorFiles<BasicTestApp.Program>();
app.UseRequestLocalization(options =>
{

View File

@ -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<BasicTestApp.Startup>();
app.UseClientSideBlazorFiles<BasicTestApp.Program>();
});
// 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<BasicTestApp.Startup>("index.html");
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Program>("index.html");
});
});
@ -56,7 +56,7 @@ namespace TestServer
app.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Startup>("test/{*path:nonfile}", "index.html");
endpoints.MapFallbackToClientSideBlazor<BasicTestApp.Program>("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");
});
});
}

View File

@ -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>("app");
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
BlazorWebAssemblyHost.CreateDefaultBuilder()
.UseBlazorStartup<Startup>();
// use builder.Services to configure application services.
#if (IndividualLocalAuth)
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddSingleton<AuthenticationStateProvider, HostAuthenticationStateProvider>();
#endif
await builder.Build().RunAsync();
}
}
}

View File

@ -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<AuthenticationStateProvider, HostAuthenticationStateProvider>();
#endif
}
public void Configure(IComponentsApplicationBuilder app)
{
app.AddComponent<App>("app");
}
}
}

View File

@ -83,7 +83,7 @@ namespace BlazorWasm_CSharp.Server
#endif
app.UseStaticFiles();
app.UseClientSideBlazorFiles<Client.Startup>();
app.UseClientSideBlazorFiles<Client.Program>();
app.UseRouting();
@ -99,7 +99,7 @@ namespace BlazorWasm_CSharp.Server
#endif
endpoints.MapControllers();
endpoints.MapFallbackToClientSideBlazor<Client.Startup>("index.html");
endpoints.MapFallbackToClientSideBlazor<Client.Program>("index.html");
});
}
}