Add BaseAddress property to WebAssemblyHostEnvironment (#20019)

- Adds `BaseAddress` to `IWebAssemblyHostEnvironment`
- Uses unmarshalled APIs to extract application host
- Move NavigationManager initialization to startup code
- Fix subdir mapping in ClientSideHostingTest

Addresses #19910
This commit is contained in:
Safia Abdalla 2020-03-23 15:48:48 -07:00 committed by GitHub
parent ba2bae80fa
commit e6078c4bdd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 98 additions and 107 deletions

View File

@ -23,6 +23,12 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
return (TResult)(object)_environment; return (TResult)(object)_environment;
case "Blazor._internal.getConfig": case "Blazor._internal.getConfig":
return (TResult)(object)null; return (TResult)(object)null;
case "Blazor._internal.navigationManager.getBaseURI":
var testUri = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests";
return (TResult)(object)testUri;
case "Blazor._internal.navigationManager.getLocationHref":
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
return (TResult)(object)testHref;
default: default:
throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'."); throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -13,8 +13,8 @@ export const internalFunctions = {
listenForNavigationEvents, listenForNavigationEvents,
enableNavigationInterception, enableNavigationInterception,
navigateTo, navigateTo,
getBaseURI: () => document.baseURI, getBaseURI: () => BINDING.js_string_to_mono_string(document.baseURI),
getLocationHref: () => location.href, getLocationHref: () => BINDING.js_string_to_mono_string(location.href),
}; };
function listenForNavigationEvents(callback: (uri: string, intercepted: boolean) => Promise<void>) { function listenForNavigationEvents(callback: (uri: string, intercepted: boolean) => Promise<void>) {
@ -141,4 +141,4 @@ function toBaseUriWithTrailingSlash(baseUri: string) {
function eventHasSpecialKey(event: MouseEvent) { function eventHasSpecialKey(event: MouseEvent) {
return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey; return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
} }

View File

@ -13,5 +13,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// Configured to "Production" when not specified by the host. /// Configured to "Production" when not specified by the host.
/// </summary> /// </summary>
string Environment { get; } string Environment { get; }
/// <summary>
/// Gets the base address for the application. This is typically derived from the "<base href>" value in the host page.
/// </summary>
string BaseAddress { get; }
} }
} }

View File

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.WebAssembly.Services; using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json; using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -55,6 +56,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
RootComponents = new RootComponentMappingCollection(); RootComponents = new RootComponentMappingCollection();
Services = new ServiceCollection(); Services = new ServiceCollection();
// Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsRuntimeInvoker);
InitializeDefaultServices(); InitializeDefaultServices();
var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker); var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
@ -66,11 +69,19 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
}; };
} }
private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
{
var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
var uri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetLocationHref, null, null, null);
WebAssemblyNavigationManager.Instance = new WebAssemblyNavigationManager(baseUri, uri);
}
private WebAssemblyHostEnvironment InitializeEnvironment(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker) private WebAssemblyHostEnvironment InitializeEnvironment(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
{ {
var applicationEnvironment = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>( var applicationEnvironment = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(
"Blazor._internal.getApplicationEnvironment", null, null, null); "Blazor._internal.getApplicationEnvironment", null, null, null);
var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment); var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment, WebAssemblyNavigationManager.Instance.BaseUri);
Services.AddSingleton<IWebAssemblyHostEnvironment>(hostEnvironment); Services.AddSingleton<IWebAssemblyHostEnvironment>(hostEnvironment);
@ -129,11 +140,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
/// <remarks> /// <remarks>
/// <para> /// <para>
/// <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> is called by <see cref="Build"/> /// <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. /// and so the delegate provided by <paramref name="configure"/> will run after all other services have been registered.
/// </para> /// </para>
/// <para> /// <para>
/// Multiple calls to <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> will replace /// Multiple calls to <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> will replace
/// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate. /// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate.
/// </para> /// </para>
/// </remarks> /// </remarks>
public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> factory, Action<TBuilder> configure = null) public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> factory, Action<TBuilder> configure = null)

View File

@ -5,8 +5,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
{ {
internal sealed class WebAssemblyHostEnvironment : IWebAssemblyHostEnvironment internal sealed class WebAssemblyHostEnvironment : IWebAssemblyHostEnvironment
{ {
public WebAssemblyHostEnvironment(string environment) => Environment = environment; public WebAssemblyHostEnvironment(string environment, string baseAddress)
{
Environment = environment;
BaseAddress = baseAddress;
}
public string Environment { get; } public string Environment { get; }
public string BaseAddress { get; }
} }
} }

View File

@ -1,31 +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.Net.Http;
using Microsoft.AspNetCore.Components;
namespace Microsoft.Extensions.DependencyInjection
{
public static class HttpClientServiceCollectionExtensions
{
/// <summary>
/// Adds a <see cref="HttpClient" /> instance to the <paramref name="serviceCollection" /> that is
/// configured to use the application's base address (<seealso cref="NavigationManager.BaseUri" />).
/// </summary>
/// <param name="serviceCollection">The <see cref="IServiceCollection" />.</param>
/// <returns>The configured <see cref="IServiceCollection" />.</returns>
public static IServiceCollection AddBaseAddressHttpClient(this IServiceCollection serviceCollection)
{
return serviceCollection.AddSingleton(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var navigationManager = s.GetRequiredService<NavigationManager>();
return new HttpClient
{
BaseAddress = new Uri(navigationManager.BaseUri)
};
});
}
}
}

View File

@ -15,21 +15,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Services
/// <summary> /// <summary>
/// Gets the instance of <see cref="WebAssemblyNavigationManager"/>. /// Gets the instance of <see cref="WebAssemblyNavigationManager"/>.
/// </summary> /// </summary>
public static readonly WebAssemblyNavigationManager Instance = new WebAssemblyNavigationManager(); public static WebAssemblyNavigationManager Instance { get; set; }
// For simplicity we force public consumption of the BrowserNavigationManager through public WebAssemblyNavigationManager(string baseUri, string uri)
// a singleton. Only a single instance can be updated by the browser through
// interop. We can construct instances for testing.
internal WebAssemblyNavigationManager()
{ {
}
protected override void EnsureInitialized()
{
// As described in the comment block above, BrowserNavigationManager is only for
// client-side (Mono) use, so it's OK to rely on synchronicity here.
var baseUri = DefaultWebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetBaseUri);
var uri = DefaultWebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetLocationHref);
Initialize(baseUri, uri); Initialize(baseUri, uri);
} }

View File

@ -7,6 +7,7 @@ using System.Text;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyModel;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Microsoft.JSInterop.WebAssembly; using Microsoft.JSInterop.WebAssembly;
@ -132,14 +133,27 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
// Arrange // Arrange
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker(environment: "Development")); var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker(environment: "Development"));
builder.Services.AddScoped<StringBuilder>();
builder.Services.AddSingleton<TestServiceThatTakesStringBuilder>();
// Assert // Assert
Assert.NotNull(builder.HostEnvironment); Assert.NotNull(builder.HostEnvironment);
Assert.True(WebAssemblyHostEnvironmentExtensions.IsDevelopment(builder.HostEnvironment)); Assert.True(WebAssemblyHostEnvironmentExtensions.IsDevelopment(builder.HostEnvironment));
} }
[Fact]
public void Builder_CreatesNavigationManager()
{
// Arrange
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker(environment: "Development"));
// Act
var host = builder.Build();
// Assert
var navigationManager = host.Services.GetRequiredService<NavigationManager>();
Assert.NotNull(navigationManager);
Assert.Equal("https://www.example.com/", navigationManager.BaseUri);
Assert.Equal("https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool", navigationManager.Uri);
}
private class TestServiceThatTakesStringBuilder private class TestServiceThatTakesStringBuilder
{ {
public TestServiceThatTakesStringBuilder(StringBuilder builder) { } public TestServiceThatTakesStringBuilder(StringBuilder builder) { }

View File

@ -1,6 +1,8 @@
// 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -13,7 +15,7 @@ namespace StandaloneApp
{ {
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app"); builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient(); builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync(); await builder.Build().RunAsync();
} }

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact] [Fact]
public void MapFallbackToClientSideBlazor_FilePath() public void MapFallbackToClientSideBlazor_FilePath()
{ {
Navigate("/filepath"); Navigate("/subdir/filepath");
WaitUntilLoaded(); WaitUntilLoaded();
Assert.NotNull(Browser.FindElement(By.Id("test-selector"))); Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
} }
@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact] [Fact]
public void MapFallbackToClientSideBlazor_Pattern_FilePath() public void MapFallbackToClientSideBlazor_Pattern_FilePath()
{ {
Navigate("/pattern_filepath/test"); Navigate("/subdir/pattern_filepath/test");
WaitUntilLoaded(); WaitUntilLoaded();
Assert.NotNull(Browser.FindElement(By.Id("test-selector"))); Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
} }
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact] [Fact]
public void MapFallbackToClientSideBlazor_AssemblyPath_FilePath() public void MapFallbackToClientSideBlazor_AssemblyPath_FilePath()
{ {
Navigate("/assemblypath_filepath"); Navigate("/subdir/assemblypath_filepath");
WaitUntilLoaded(); WaitUntilLoaded();
Assert.NotNull(Browser.FindElement(By.Id("test-selector"))); Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
} }
@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact] [Fact]
public void MapFallbackToClientSideBlazor_AssemblyPath_Pattern_FilePath() public void MapFallbackToClientSideBlazor_AssemblyPath_Pattern_FilePath()
{ {
Navigate("/assemblypath_pattern_filepath/test"); Navigate("/subdir/assemblypath_pattern_filepath/test");
WaitUntilLoaded(); WaitUntilLoaded();
Assert.NotNull(Browser.FindElement(By.Id("test-selector"))); Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
} }

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using BasicTestApp.AuthTest; using BasicTestApp.AuthTest;
@ -36,7 +37,7 @@ namespace BasicTestApp
builder.RootComponents.Add<Index>("root"); builder.RootComponents.Add<Index>("root");
builder.Services.AddBaseAddressHttpClient(); builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>(); builder.Services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
builder.Services.AddAuthorizationCore(options => builder.Services.AddAuthorizationCore(options =>
{ {

View File

@ -23,6 +23,7 @@ namespace TestServer
["Server authentication"] = (BuildWebHost<ServerAuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"), ["Server authentication"] = (BuildWebHost<ServerAuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"),
["CORS (WASM)"] = (BuildWebHost<CorsStartup>(CreateAdditionalArgs(args)), "/subdir"), ["CORS (WASM)"] = (BuildWebHost<CorsStartup>(CreateAdditionalArgs(args)), "/subdir"),
["Prerendering (Server-side)"] = (BuildWebHost<PrerenderedStartup>(CreateAdditionalArgs(args)), "/prerendered"), ["Prerendering (Server-side)"] = (BuildWebHost<PrerenderedStartup>(CreateAdditionalArgs(args)), "/prerendered"),
["Client-side with fallback"] = (BuildWebHost<StartupWithMapFallbackToClientSideBlazor>(CreateAdditionalArgs(args)), "/fallback"),
["Multiple components (Server-side)"] = (BuildWebHost<MultipleComponents>(CreateAdditionalArgs(args)), "/multiple-components"), ["Multiple components (Server-side)"] = (BuildWebHost<MultipleComponents>(CreateAdditionalArgs(args)), "/multiple-components"),
["Globalization + Localization (Server-side)"] = (BuildWebHost<InternationalizationStartup>(CreateAdditionalArgs(args)), "/subdir"), ["Globalization + Localization (Server-side)"] = (BuildWebHost<InternationalizationStartup>(CreateAdditionalArgs(args)), "/subdir"),
["Server-side blazor"] = (BuildWebHost<ServerStartup>(CreateAdditionalArgs(args)), "/subdir"), ["Server-side blazor"] = (BuildWebHost<ServerStartup>(CreateAdditionalArgs(args)), "/subdir"),

View File

@ -31,50 +31,43 @@ namespace TestServer
} }
// The client-side files middleware needs to be here because the base href in hardcoded to /subdir/ // The client-side files middleware needs to be here because the base href in hardcoded to /subdir/
app.Map("/subdir", app => app.Map("/subdir", subApp =>
{ {
app.UseBlazorFrameworkFiles(); subApp.UseBlazorFrameworkFiles();
app.UseStaticFiles(); subApp.UseStaticFiles();
});
// The calls to `Map` allow us to test each of these overloads, while keeping them isolated. // The calls to `Map` allow us to test each of these overloads, while keeping them isolated.
app.Map("/filepath", app => subApp.Map("/filepath", filepath =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToFile("index.html"); filepath.UseRouting();
filepath.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToFile("index.html");
});
}); });
}); subApp.Map("/pattern_filepath", patternFilePath =>
app.Map("/pattern_filepath", app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html"); patternFilePath.UseRouting();
patternFilePath.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
});
}); });
}); subApp.Map("/assemblypath_filepath", assemblyPathFilePath =>
app.Map("/assemblypath_filepath", app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToFile("index.html"); assemblyPathFilePath.UseRouting();
assemblyPathFilePath.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToFile("index.html");
});
}); });
}); subApp.Map("/assemblypath_pattern_filepath", assemblyPatternFilePath =>
app.Map("/assemblypath_pattern_filepath", app =>
{
app.UseRouting();
app.UseEndpoints(endpoints =>
{ {
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html"); assemblyPatternFilePath.UseRouting();
assemblyPatternFilePath.UseEndpoints(endpoints =>
{
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
});
}); });
}); });
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Text; using System.Text;
@ -18,7 +19,7 @@ namespace ComponentsWebAssembly_CSharp
var builder = WebAssemblyHostBuilder.CreateDefault(args); var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app"); builder.RootComponents.Add<App>("app");
builder.Services.AddBaseAddressHttpClient(); builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
#if (IndividualLocalAuth) #if (IndividualLocalAuth)
#if (Hosted) #if (Hosted)
builder.Services.AddApiAuthorization(); builder.Services.AddApiAuthorization();