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:
parent
ba08d60cb9
commit
3111032dc0
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace TestServer
|
|||
app.Map("/subdir", app =>
|
||||
{
|
||||
app.UseStaticFiles();
|
||||
app.UseClientSideBlazorFiles<BasicTestApp.Startup>();
|
||||
app.UseClientSideBlazorFiles<BasicTestApp.Program>();
|
||||
|
||||
app.UseRequestLocalization(options =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue