-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Goals
- Reduce complexity/ceremony of startup/main code for Blazor WASM
- Address gaps in current scenarios (no way to get services before UI is shown)
- non-goal symmetry or consistency with server-side ASP.NET Core
Sample Code
These are some focused samples with my proposed implementation choice. There's another section with alternatives and why I didn't choose them. I think there are a lot of reasonable small tweaks to this, but I wanted to lead with something.
Adding configuration to the mix opens a few questions which need further discussion, so it's a good thing we're injecting it at this point.
Simple
This is the code that we expect to appear in every template.
async static Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
await builder.Build().RunAsync();
}Adding custom initialization
This adds some custom initialization after components are registered and after services are available, but before the UI is shown.
async static Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddSingleton<WeatherService>();
builder.RootComponents.Add<App>("app");
var host = builder.Build();
var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeAsync();
await host.RunAsync();
}Adding configuration
This adds configuration, which the user can use in their initialization code.
async static Task MainWithConfig(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddSingleton<WeatherService>();
builder.RootComponents.Add<App>("app");
var host = builder.Build();
var weatherService = host.Services.GetRequiredService<WeatherService>();
await weatherService.InitializeAsync(host.Configuration["WeatherUrl"]);
await host.RunAsync();
}Open questions
- Should we rename one of the two
.Servicesproperties for clarity? One isIServiceCollection, the other isIServiceProvider. I like the idea of this in general, but I haven't come up with a pair of names that feel good. - What's the cost (in size) of including
IConfigurationandIConfigurationBuilder?
Options and inflection points
StartAsync/StopAsync vs RunAsync
The ASP.NET Core hosting stuff has start/stop in addition to run. It's a source of confusion, start/stop are a lot more subtle than they seem, so I didn't plan to add them here.
Configuration as part of WebAssemblyHostBuilder
One of the negatives of putting configuration here is that it won't/can't be linked out without a look of gymnastics. I think it's worth it though from a design point of view.
You could also imagine an alternative where we don't tie ourselves to configuration at all (nowhere on our API surface). The guidance for users could be to add it to DI themselves.
ComponentMappingCollection vs methods on the WebAssemblyHostBuilder
We discussed this idea last time, so I wanted to include it so we can compare. I like the property a little bit better.
Disposing the host
NOTE: host.RunAsync() will dispose the host if/when RunAsync terminates. This is a little bit bizarre, but it's what generic host does. Hosts are single-use. It's not clear in the WASM how that RunAsync will terminate, at least not in this release.
Apis
namespace Microsoft.??????
{
public sealed class WebAssemblyHostBuilder
{
public static WebAssemblyHostBuilder CreateDefault(string[] args) => throw null;
public WebAssemblyEnvironment Environment { get; }
public IServiceCollection Services { get; }
public IConfigurationBuilder Configuration { get; }
public ComponentMappingCollection RootComponents { get; }
public WebAssemblyHost Build() => throw null;
}
public sealed class WebAssemblyEnvironment
{
public string BaseUri { get; }
}
public sealed class WebAssemblyHost : IDisposable
{
public IConfiguration Configuration { get; }
public IServiceProvider Services { get; }
public void Dispose()
{
throw new NotImplementedException();
}
public Task RunAsync() => throw null;
}
public sealed class ComponentMappingCollection : Collection<ComponentMapping>
{
public void Add<TComponent>(string selector) where TComponent : IComponent => throw null;
public void Add(string selector, Type componentType) => throw null;
}
public readonly struct ComponentMapping
{
public ComponentMapping(string selector, Type componentType)
{
Selector = selector;
ComponentType = componentType;
}
public string Selector { get; }
public Type ComponentType { get; }
}
}