Add service provider factory support

Fixes: #18814

This adds back support on the Blazor WASM Host for using
ISerivceProviderFactory<>.

We previously had this support when the Blazor WASM host was a clone of
generic host, but I accidentally lost it when simplifying the host
(sorry :( ).
This commit is contained in:
Ryan Nowak 2020-02-08 12:43:56 -08:00
parent e8d2d562aa
commit 9c16db3e89
2 changed files with 112 additions and 2 deletions

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.Configuration;
@ -20,6 +19,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// </summary>
public sealed class WebAssemblyHostBuilder
{
private Func<IServiceProvider> _createServiceProvider;
/// <summary>
/// Creates an instance of <see cref="WebAssemblyHostBuilder"/> using the most common
/// conventions and settings.
@ -53,6 +54,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Services = new ServiceCollection();
InitializeDefaultServices();
_createServiceProvider = () =>
{
return Services.BuildServiceProvider();
};
}
/// <summary>
@ -71,6 +77,40 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// </summary>
public IServiceCollection Services { get; }
/// <summary>
/// Registers a <see cref="IServiceProviderFactory{TBuilder}" /> instance to be used to create the <see cref="IServiceProvider" />.
/// </summary>
/// <param name="factory">The <see cref="IServiceProviderFactory{TBuilder}" />.</param>
/// <param name="configure">
/// A delegate used to configure the <typeparamref T="TBuilder" />. This can be used to configure services using
/// APIS specific to the <see cref="IServiceProviderFactory{TBuilder}" /> implementation.
/// </param>
/// <typeparam name="TBuilder">The type of builder provided by the <see cref="IServiceProviderFactory{TBuilder}" />.</typeparam>
/// <remarks>
/// <para>
/// <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> is called by <see cref="Build"/>
/// and so the delegate provided by <paramref name="configure"/> will run after all other services have been registered.
/// </para>
/// <para>
/// Multiple calls to <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> will replace
/// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate.
/// </para>
/// </remarks>
public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> factory, Action<TBuilder> configure = null)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
_createServiceProvider = () =>
{
var container = factory.CreateBuilder(Services);
configure?.Invoke(container);
return factory.CreateServiceProvider(container);
};
}
/// <summary>
/// Builds a <see cref="WebAssemblyHost"/> instance based on the configuration of this builder.
/// </summary>
@ -84,7 +124,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
// 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 services = _createServiceProvider();
var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope();
return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray());

View File

@ -51,6 +51,76 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
}
[Fact]
public void Build_AllowsConfiguringContainer()
{
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
builder.Services.AddScoped<StringBuilder>();
var factory = new MyFakeServiceProviderFactory();
builder.ConfigureContainer(factory);
// Act
var host = builder.Build();
// Assert
Assert.True(factory.CreateServiceProviderCalled);
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
}
[Fact]
public void Build_AllowsConfiguringContainer_WithDelegate()
{
// Arrange
var builder = WebAssemblyHostBuilder.CreateDefault();
builder.Services.AddScoped<StringBuilder>();
var factory = new MyFakeServiceProviderFactory();
builder.ConfigureContainer(factory, builder =>
{
builder.ServiceCollection.AddScoped<List<string>>();
});
// Act
var host = builder.Build();
// Assert
Assert.True(factory.CreateServiceProviderCalled);
Assert.NotNull(host.Services.GetRequiredService<StringBuilder>());
Assert.NotNull(host.Services.GetRequiredService<List<string>>());
}
private class MyFakeDIBuilderThing
{
public MyFakeDIBuilderThing(IServiceCollection serviceCollection)
{
ServiceCollection = serviceCollection;
}
public IServiceCollection ServiceCollection { get; }
}
private class MyFakeServiceProviderFactory : IServiceProviderFactory<MyFakeDIBuilderThing>
{
public bool CreateServiceProviderCalled { get; set; }
public MyFakeDIBuilderThing CreateBuilder(IServiceCollection services)
{
return new MyFakeDIBuilderThing(services);
}
public IServiceProvider CreateServiceProvider(MyFakeDIBuilderThing containerBuilder)
{
// This is the best way to test the factory was actually used. The Host doesn't
// expose the *root* service provider, only a scoped instance. So we can return
// a different type here, but we have no way to inspect it.
CreateServiceProviderCalled = true;
return containerBuilder.ServiceCollection.BuildServiceProvider();
}
}
[Fact]
public void Build_AddsConfigurationToServices()
{