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:
Ryan Nowak 2018-06-22 16:23:43 -07:00
parent ed57767e6a
commit ee62bd8d45
19 changed files with 1040 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>

View File

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

View File

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

View File

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