Add startup for client/mono Blazor
This change adds a host builder, and the startup pattern for client-side Blazor apps running in mono/wasm. This will help us align better with server side Blazor.
This commit is contained in:
parent
ed57767e6a
commit
ee62bd8d45
|
|
@ -1,8 +1,7 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// 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.Blazor.Browser.Rendering;
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Services;
|
||||
using Microsoft.AspNetCore.Blazor.Hosting;
|
||||
|
||||
namespace StandaloneApp
|
||||
{
|
||||
|
|
@ -10,12 +9,11 @@ namespace StandaloneApp
|
|||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
var serviceProvider = new BrowserServiceProvider(configure =>
|
||||
{
|
||||
// Add any custom services here
|
||||
});
|
||||
|
||||
new BrowserRenderer(serviceProvider).AddComponent<App>("app");
|
||||
CreateHostBuilder(args).Build().Run();
|
||||
}
|
||||
|
||||
public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
|
||||
BlazorWebAssemblyHost.CreateDefaultBuilder()
|
||||
.UseBlazorStartup<Startup>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
// 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.Blazor.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace StandaloneApp
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
|
||||
public void Configure(IBlazorApplicationBuilder app)
|
||||
{
|
||||
app.AddComponent<App>("app");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// 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.Blazor.Components;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="IBlazorApplicationBuilder"/>.
|
||||
/// </summary>
|
||||
public static class BlazorApplicationBuilderExtensions
|
||||
{
|
||||
/// <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="IBlazorApplicationBuilder"/>.</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 IBlazorApplicationBuilder app, string domElementSelector)
|
||||
where TComponent : IComponent
|
||||
{
|
||||
app.AddComponent(typeof(TComponent), domElementSelector);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// 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.Blazor.Components;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Builder
|
||||
{
|
||||
/// <summary>
|
||||
/// A builder for constructing a Blazor application.
|
||||
/// </summary>
|
||||
public interface IBlazorApplicationBuilder
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// 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.Browser.Rendering;
|
||||
using Microsoft.AspNetCore.Blazor.Builder;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
internal class WebAssemblyBlazorApplicationBuilder : IBlazorApplicationBuilder
|
||||
{
|
||||
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 BrowserRenderer CreateRenderer()
|
||||
{
|
||||
var renderer = new BrowserRenderer(Services);
|
||||
for (var i = 0; i < Entries.Count; i++)
|
||||
{
|
||||
var entry = Entries[i];
|
||||
renderer.AddComponent(entry.componentType, entry.domElementSelector);
|
||||
}
|
||||
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// 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 to create instances a Blazor host builder for a Browser application.
|
||||
/// </summary>
|
||||
public static class BlazorWebAssemblyHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a an instance of <see cref="IWebAssemblyHostBuilder"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <see cref="IWebAssemblyHostBuilder"/>.</returns>
|
||||
public static IWebAssemblyHostBuilder CreateDefaultBuilder()
|
||||
{
|
||||
return new WebAssemblyHostBuilder();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
// 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.Blazor.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
|
||||
{
|
||||
private readonly object _instance;
|
||||
|
||||
public ConventionBasedStartup(object instance)
|
||||
{
|
||||
_instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
}
|
||||
|
||||
public void Configure(IBlazorApplicationBuilder 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(IBlazorApplicationBuilder)
|
||||
? 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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// 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.Blazor.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
internal interface IBlazorStartup
|
||||
{
|
||||
void ConfigureServices(IServiceCollection services);
|
||||
|
||||
void Configure(IBlazorApplicationBuilder app, IServiceProvider services);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
// 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>
|
||||
/// 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
// 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;
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Rendering;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
internal class WebAssemblyHost : IWebAssemblyHost
|
||||
{
|
||||
private readonly IJSRuntime _runtime;
|
||||
|
||||
private IServiceScope _scope;
|
||||
private BrowserRenderer _renderer;
|
||||
|
||||
public WebAssemblyHost(IServiceProvider services, IJSRuntime runtime)
|
||||
{
|
||||
Services = services ?? throw new ArgumentNullException(nameof(services));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
}
|
||||
|
||||
public IServiceProvider Services { get; }
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// We need to do this as early as possible, it eliminates a bunch of problems. Note that what we do
|
||||
// is a bit fragile. If you see things breaking because JSRuntime.Current isn't set, then it's likely
|
||||
// that something on the startup path went wrong.
|
||||
//
|
||||
// We want to the JSRuntime created here to be the 'ambient' runtime when JS calls back into .NET. When
|
||||
// this happens in the browser it will be a direct call from Mono. We effectively needs to set the
|
||||
// JSRuntime in the 'root' execution context which implies that we want to do as part of a direct
|
||||
// call from Program.Main, and before any 'awaits'.
|
||||
JSRuntime.SetCurrentJSRuntime(_runtime);
|
||||
|
||||
var scopeFactory = Services.GetRequiredService<IServiceScopeFactory>();
|
||||
_scope = scopeFactory.CreateScope();
|
||||
|
||||
try
|
||||
{
|
||||
var startup = _scope.ServiceProvider.GetService<IBlazorStartup>();
|
||||
if (startup == null)
|
||||
{
|
||||
var message =
|
||||
$"Could not find a registered Blazor Startup class. " +
|
||||
$"Using {nameof(IWebAssemblyHost)} requires a call to {nameof(IWebAssemblyHostBuilder)}.UseBlazorStartup.";
|
||||
throw new InvalidOperationException(message);
|
||||
}
|
||||
|
||||
// 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 = builder.CreateRenderer();
|
||||
}
|
||||
catch
|
||||
{
|
||||
_scope.Dispose();
|
||||
_scope = null;
|
||||
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.Dispose();
|
||||
_renderer = null;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
// 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.Net.Http;
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Http;
|
||||
using Microsoft.AspNetCore.Blazor.Browser.Services;
|
||||
using Microsoft.AspNetCore.Blazor.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.JSInterop;
|
||||
using Mono.WebAssembly.Interop;
|
||||
|
||||
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
|
||||
{
|
||||
private List<Action<WebAssemblyHostBuilderContext, IServiceCollection>> _configureServicesActions = new List<Action<WebAssemblyHostBuilderContext, IServiceCollection>>();
|
||||
private bool _hostBuilt;
|
||||
private WebAssemblyHostBuilderContext _BrowserHostBuilderContext;
|
||||
private IServiceProvider _appServices;
|
||||
|
||||
/// <summary>
|
||||
/// A central location for sharing state between components during the host building process.
|
||||
/// </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)
|
||||
{
|
||||
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the given actions to initialize the host. This can only be called once.
|
||||
/// </summary>
|
||||
/// <returns>An initialized <see cref="IWebAssemblyHost"/></returns>
|
||||
public IWebAssemblyHost Build()
|
||||
{
|
||||
if (_hostBuilt)
|
||||
{
|
||||
throw new InvalidOperationException("Build can only be called once.");
|
||||
}
|
||||
_hostBuilt = true;
|
||||
|
||||
CreateBrowserHostBuilderContext();
|
||||
CreateServiceProvider();
|
||||
|
||||
return _appServices.GetRequiredService<IWebAssemblyHost>();
|
||||
}
|
||||
|
||||
private void CreateBrowserHostBuilderContext()
|
||||
{
|
||||
_BrowserHostBuilderContext = new WebAssemblyHostBuilderContext(Properties);
|
||||
}
|
||||
|
||||
private void CreateServiceProvider()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddSingleton(_BrowserHostBuilderContext);
|
||||
services.AddSingleton<IWebAssemblyHost, WebAssemblyHost>();
|
||||
services.AddSingleton<IJSRuntime, MonoWebAssemblyJSRuntime>();
|
||||
|
||||
services.AddSingleton<IUriHelper, BrowserUriHelper>();
|
||||
services.AddSingleton<HttpClient>(s =>
|
||||
{
|
||||
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
|
||||
var uriHelper = s.GetRequiredService<IUriHelper>();
|
||||
return new HttpClient(new BrowserHttpMessageHandler())
|
||||
{
|
||||
BaseAddress = new Uri(uriHelper.GetBaseUri())
|
||||
};
|
||||
});
|
||||
|
||||
foreach (var configureServicesAction in _configureServicesActions)
|
||||
{
|
||||
configureServicesAction(_BrowserHostBuilderContext, services);
|
||||
}
|
||||
|
||||
_appServices = services.BuildServiceProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// 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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// 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>
|
||||
/// 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)
|
||||
{
|
||||
host.StartAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,209 @@
|
|||
// 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.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.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(IBlazorApplicationBuilder app, string foo)
|
||||
{
|
||||
Arguments.Add(app);
|
||||
Arguments.Add(foo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
// 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.Extensions.DependencyInjection;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
public class WebAssemblyHostBuilderTest
|
||||
{
|
||||
[Fact]
|
||||
public void HostBuilder_CanCallBuild_BuildsServices()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder();
|
||||
|
||||
// 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)
|
||||
{
|
||||
services.AddSingleton<string>("foo");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
// 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.Tasks;
|
||||
using Microsoft.AspNetCore.Blazor.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.JSInterop;
|
||||
using Mono.WebAssembly.Interop;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Hosting
|
||||
{
|
||||
public class WebAssemblyHostTest
|
||||
{
|
||||
[Fact]
|
||||
public async Task BrowserHost_StartAsync_ThrowsWithoutStartup()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder();
|
||||
var host = builder.Build();
|
||||
|
||||
// Act
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await host.StartAsync());
|
||||
|
||||
// Assert
|
||||
Assert.Equal(
|
||||
"Could not find a registered Blazor Startup class. " +
|
||||
"Using IWebAssemblyHost requires a call to IWebAssemblyHostBuilder.UseBlazorStartup.",
|
||||
ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BrowserHost_StartAsync_RunsConfigureMethod()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder();
|
||||
|
||||
var startup = new MockStartup();
|
||||
builder.ConfigureServices((c, s) => { s.AddSingleton<IBlazorStartup>(startup); });
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// Act
|
||||
await host.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(startup.ConfigureCalled);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BrowserHost_StartAsync_SetsJSRuntime()
|
||||
{
|
||||
// Arrange
|
||||
var builder = new WebAssemblyHostBuilder();
|
||||
builder.UseBlazorStartup<MockStartup>();
|
||||
|
||||
var host = builder.Build();
|
||||
|
||||
// Act
|
||||
await host.StartAsync();
|
||||
|
||||
// Assert
|
||||
Assert.IsType<MonoWebAssemblyJSRuntime>(JSRuntime.Current);
|
||||
}
|
||||
|
||||
private class MockStartup : IBlazorStartup
|
||||
{
|
||||
public bool ConfigureCalled { get; set; }
|
||||
|
||||
public void Configure(IBlazorApplicationBuilder app, IServiceProvider services)
|
||||
{
|
||||
ConfigureCalled = true;
|
||||
}
|
||||
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue