Skip to content

Commit e6078c4

Browse files
authored
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
1 parent ba2bae8 commit e6078c4

File tree

16 files changed

+98
-107
lines changed

16 files changed

+98
-107
lines changed

src/Components/Shared/test/TestWebAssemblyJSRuntimeInvoker.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ public override TResult InvokeUnmarshalled<T0, T1, T2, TResult>(string identifie
2323
return (TResult)(object)_environment;
2424
case "Blazor._internal.getConfig":
2525
return (TResult)(object)null;
26+
case "Blazor._internal.navigationManager.getBaseURI":
27+
var testUri = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests";
28+
return (TResult)(object)testUri;
29+
case "Blazor._internal.navigationManager.getLocationHref":
30+
var testHref = "https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool";
31+
return (TResult)(object)testHref;
2632
default:
2733
throw new NotImplementedException($"{nameof(TestWebAssemblyJSRuntimeInvoker)} has no implementation for '{identifier}'.");
2834
}

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 3 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webassembly.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Services/NavigationManager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ export const internalFunctions = {
1313
listenForNavigationEvents,
1414
enableNavigationInterception,
1515
navigateTo,
16-
getBaseURI: () => document.baseURI,
17-
getLocationHref: () => location.href,
16+
getBaseURI: () => BINDING.js_string_to_mono_string(document.baseURI),
17+
getLocationHref: () => BINDING.js_string_to_mono_string(location.href),
1818
};
1919

2020
function listenForNavigationEvents(callback: (uri: string, intercepted: boolean) => Promise<void>) {
@@ -141,4 +141,4 @@ function toBaseUriWithTrailingSlash(baseUri: string) {
141141

142142
function eventHasSpecialKey(event: MouseEvent) {
143143
return event.ctrlKey || event.shiftKey || event.altKey || event.metaKey;
144-
}
144+
}

src/Components/WebAssembly/WebAssembly/src/Hosting/IWebAssemblyHostEnvironment.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@ public interface IWebAssemblyHostEnvironment
1313
/// Configured to "Production" when not specified by the host.
1414
/// </summary>
1515
string Environment { get; }
16+
17+
/// <summary>
18+
/// Gets the base address for the application. This is typically derived from the "<base href>" value in the host page.
19+
/// </summary>
20+
string BaseAddress { get; }
1621
}
1722
}

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using Microsoft.AspNetCore.Components.Routing;
99
using Microsoft.AspNetCore.Components.WebAssembly.Services;
10+
using Microsoft.AspNetCore.Components.Web;
1011
using Microsoft.Extensions.Configuration;
1112
using Microsoft.Extensions.Configuration.Json;
1213
using Microsoft.Extensions.DependencyInjection;
@@ -55,6 +56,8 @@ internal WebAssemblyHostBuilder(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
5556
RootComponents = new RootComponentMappingCollection();
5657
Services = new ServiceCollection();
5758

59+
// Retrieve required attributes from JSRuntimeInvoker
60+
InitializeNavigationManager(jsRuntimeInvoker);
5861
InitializeDefaultServices();
5962

6063
var hostEnvironment = InitializeEnvironment(jsRuntimeInvoker);
@@ -66,11 +69,19 @@ internal WebAssemblyHostBuilder(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
6669
};
6770
}
6871

72+
private void InitializeNavigationManager(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
73+
{
74+
var baseUri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetBaseUri, null, null, null);
75+
var uri = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(BrowserNavigationManagerInterop.GetLocationHref, null, null, null);
76+
77+
WebAssemblyNavigationManager.Instance = new WebAssemblyNavigationManager(baseUri, uri);
78+
}
79+
6980
private WebAssemblyHostEnvironment InitializeEnvironment(WebAssemblyJSRuntimeInvoker jsRuntimeInvoker)
7081
{
7182
var applicationEnvironment = jsRuntimeInvoker.InvokeUnmarshalled<object, object, object, string>(
7283
"Blazor._internal.getApplicationEnvironment", null, null, null);
73-
var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment);
84+
var hostEnvironment = new WebAssemblyHostEnvironment(applicationEnvironment, WebAssemblyNavigationManager.Instance.BaseUri);
7485

7586
Services.AddSingleton<IWebAssemblyHostEnvironment>(hostEnvironment);
7687

@@ -129,11 +140,11 @@ private WebAssemblyHostEnvironment InitializeEnvironment(WebAssemblyJSRuntimeInv
129140
/// <remarks>
130141
/// <para>
131142
/// <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> is called by <see cref="Build"/>
132-
/// and so the delegate provided by <paramref name="configure"/> will run after all other services have been registered.
143+
/// and so the delegate provided by <paramref name="configure"/> will run after all other services have been registered.
133144
/// </para>
134145
/// <para>
135146
/// Multiple calls to <see cref="ConfigureContainer{TBuilder}(IServiceProviderFactory{TBuilder}, Action{TBuilder})"/> will replace
136-
/// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate.
147+
/// the previously stored <paramref name="factory"/> and <paramref name="configure"/> delegate.
137148
/// </para>
138149
/// </remarks>
139150
public void ConfigureContainer<TBuilder>(IServiceProviderFactory<TBuilder> factory, Action<TBuilder> configure = null)

src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostEnvironment.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
55
{
66
internal sealed class WebAssemblyHostEnvironment : IWebAssemblyHostEnvironment
77
{
8-
public WebAssemblyHostEnvironment(string environment) => Environment = environment;
8+
public WebAssemblyHostEnvironment(string environment, string baseAddress)
9+
{
10+
Environment = environment;
11+
BaseAddress = baseAddress;
12+
}
913

1014
public string Environment { get; }
15+
16+
public string BaseAddress { get; }
1117
}
1218
}

src/Components/WebAssembly/WebAssembly/src/Services/HttpClientServiceCollectionExtensions.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,10 @@ internal class WebAssemblyNavigationManager : NavigationManager
1515
/// <summary>
1616
/// Gets the instance of <see cref="WebAssemblyNavigationManager"/>.
1717
/// </summary>
18-
public static readonly WebAssemblyNavigationManager Instance = new WebAssemblyNavigationManager();
18+
public static WebAssemblyNavigationManager Instance { get; set; }
1919

20-
// For simplicity we force public consumption of the BrowserNavigationManager through
21-
// a singleton. Only a single instance can be updated by the browser through
22-
// interop. We can construct instances for testing.
23-
internal WebAssemblyNavigationManager()
20+
public WebAssemblyNavigationManager(string baseUri, string uri)
2421
{
25-
}
26-
27-
protected override void EnsureInitialized()
28-
{
29-
// As described in the comment block above, BrowserNavigationManager is only for
30-
// client-side (Mono) use, so it's OK to rely on synchronicity here.
31-
var baseUri = DefaultWebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetBaseUri);
32-
var uri = DefaultWebAssemblyJSRuntime.Instance.Invoke<string>(Interop.GetLocationHref);
3322
Initialize(baseUri, uri);
3423
}
3524

src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostBuilderTest.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Components.Routing;
88
using Microsoft.Extensions.Configuration;
99
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.DependencyModel;
1011
using Microsoft.Extensions.Logging;
1112
using Microsoft.JSInterop;
1213
using Microsoft.JSInterop.WebAssembly;
@@ -132,14 +133,27 @@ public void Builder_InDevelopment_SetsHostEnvironmentProperty()
132133
// Arrange
133134
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker(environment: "Development"));
134135

135-
builder.Services.AddScoped<StringBuilder>();
136-
builder.Services.AddSingleton<TestServiceThatTakesStringBuilder>();
137-
138136
// Assert
139137
Assert.NotNull(builder.HostEnvironment);
140138
Assert.True(WebAssemblyHostEnvironmentExtensions.IsDevelopment(builder.HostEnvironment));
141139
}
142140

141+
[Fact]
142+
public void Builder_CreatesNavigationManager()
143+
{
144+
// Arrange
145+
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker(environment: "Development"));
146+
147+
// Act
148+
var host = builder.Build();
149+
150+
// Assert
151+
var navigationManager = host.Services.GetRequiredService<NavigationManager>();
152+
Assert.NotNull(navigationManager);
153+
Assert.Equal("https://www.example.com/", navigationManager.BaseUri);
154+
Assert.Equal("https://www.example.com/awesome-part-that-will-be-truncated-in-tests/cool", navigationManager.Uri);
155+
}
156+
143157
private class TestServiceThatTakesStringBuilder
144158
{
145159
public TestServiceThatTakesStringBuilder(StringBuilder builder) { }

src/Components/WebAssembly/testassets/StandaloneApp/Program.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
5+
using System.Net.Http;
46
using System.Threading.Tasks;
57
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
68
using Microsoft.Extensions.DependencyInjection;
@@ -13,7 +15,7 @@ public static async Task Main(string[] args)
1315
{
1416
var builder = WebAssemblyHostBuilder.CreateDefault(args);
1517
builder.RootComponents.Add<App>("app");
16-
builder.Services.AddBaseAddressHttpClient();
18+
builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
1719

1820
await builder.Build().RunAsync();
1921
}

src/Components/test/E2ETest/Tests/ClientSideHostingTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,31 @@ public ClientSideHostingTest(
3232
[Fact]
3333
public void MapFallbackToClientSideBlazor_FilePath()
3434
{
35-
Navigate("/filepath");
35+
Navigate("/subdir/filepath");
3636
WaitUntilLoaded();
3737
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
3838
}
3939

4040
[Fact]
4141
public void MapFallbackToClientSideBlazor_Pattern_FilePath()
4242
{
43-
Navigate("/pattern_filepath/test");
43+
Navigate("/subdir/pattern_filepath/test");
4444
WaitUntilLoaded();
4545
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
4646
}
4747

4848
[Fact]
4949
public void MapFallbackToClientSideBlazor_AssemblyPath_FilePath()
5050
{
51-
Navigate("/assemblypath_filepath");
51+
Navigate("/subdir/assemblypath_filepath");
5252
WaitUntilLoaded();
5353
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
5454
}
5555

5656
[Fact]
5757
public void MapFallbackToClientSideBlazor_AssemblyPath_Pattern_FilePath()
5858
{
59-
Navigate("/assemblypath_pattern_filepath/test");
59+
Navigate("/subdir/assemblypath_pattern_filepath/test");
6060
WaitUntilLoaded();
6161
Assert.NotNull(Browser.FindElement(By.Id("test-selector")));
6262
}

src/Components/test/testassets/BasicTestApp/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Globalization;
66
using System.Linq;
7+
using System.Net.Http;
78
using System.Runtime.InteropServices;
89
using System.Threading.Tasks;
910
using BasicTestApp.AuthTest;
@@ -36,7 +37,7 @@ public static async Task Main(string[] args)
3637

3738
builder.RootComponents.Add<Index>("root");
3839

39-
builder.Services.AddBaseAddressHttpClient();
40+
builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
4041
builder.Services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
4142
builder.Services.AddAuthorizationCore(options =>
4243
{

src/Components/test/testassets/TestServer/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static void Main(string[] args)
2323
["Server authentication"] = (BuildWebHost<ServerAuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"),
2424
["CORS (WASM)"] = (BuildWebHost<CorsStartup>(CreateAdditionalArgs(args)), "/subdir"),
2525
["Prerendering (Server-side)"] = (BuildWebHost<PrerenderedStartup>(CreateAdditionalArgs(args)), "/prerendered"),
26+
["Client-side with fallback"] = (BuildWebHost<StartupWithMapFallbackToClientSideBlazor>(CreateAdditionalArgs(args)), "/fallback"),
2627
["Multiple components (Server-side)"] = (BuildWebHost<MultipleComponents>(CreateAdditionalArgs(args)), "/multiple-components"),
2728
["Globalization + Localization (Server-side)"] = (BuildWebHost<InternationalizationStartup>(CreateAdditionalArgs(args)), "/subdir"),
2829
["Server-side blazor"] = (BuildWebHost<ServerStartup>(CreateAdditionalArgs(args)), "/subdir"),

src/Components/test/testassets/TestServer/StartupWithMapFallbackToClientSideBlazor.cs

Lines changed: 28 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,50 +31,43 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
3131
}
3232

3333
// The client-side files middleware needs to be here because the base href in hardcoded to /subdir/
34-
app.Map("/subdir", app =>
34+
app.Map("/subdir", subApp =>
3535
{
36-
app.UseBlazorFrameworkFiles();
37-
app.UseStaticFiles();
38-
});
39-
40-
// The calls to `Map` allow us to test each of these overloads, while keeping them isolated.
41-
app.Map("/filepath", app =>
42-
{
43-
app.UseRouting();
36+
subApp.UseBlazorFrameworkFiles();
37+
subApp.UseStaticFiles();
4438

45-
app.UseEndpoints(endpoints =>
39+
// The calls to `Map` allow us to test each of these overloads, while keeping them isolated.
40+
subApp.Map("/filepath", filepath =>
4641
{
47-
endpoints.MapFallbackToFile("index.html");
42+
filepath.UseRouting();
43+
filepath.UseEndpoints(endpoints =>
44+
{
45+
endpoints.MapFallbackToFile("index.html");
46+
});
4847
});
49-
});
50-
51-
app.Map("/pattern_filepath", app =>
52-
{
53-
app.UseRouting();
54-
55-
app.UseEndpoints(endpoints =>
48+
subApp.Map("/pattern_filepath", patternFilePath =>
5649
{
57-
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
50+
patternFilePath.UseRouting();
51+
patternFilePath.UseEndpoints(endpoints =>
52+
{
53+
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
54+
});
5855
});
59-
});
60-
61-
app.Map("/assemblypath_filepath", app =>
62-
{
63-
app.UseRouting();
64-
65-
app.UseEndpoints(endpoints =>
56+
subApp.Map("/assemblypath_filepath", assemblyPathFilePath =>
6657
{
67-
endpoints.MapFallbackToFile("index.html");
58+
assemblyPathFilePath.UseRouting();
59+
assemblyPathFilePath.UseEndpoints(endpoints =>
60+
{
61+
endpoints.MapFallbackToFile("index.html");
62+
});
6863
});
69-
});
70-
71-
app.Map("/assemblypath_pattern_filepath", app =>
72-
{
73-
app.UseRouting();
74-
75-
app.UseEndpoints(endpoints =>
64+
subApp.Map("/assemblypath_pattern_filepath", assemblyPatternFilePath =>
7665
{
77-
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
66+
assemblyPatternFilePath.UseRouting();
67+
assemblyPatternFilePath.UseEndpoints(endpoints =>
68+
{
69+
endpoints.MapFallbackToFile("test/{*path:nonfile}", "index.html");
70+
});
7871
});
7972
});
8073
}

src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Net.Http;
23
using System.Collections.Generic;
34
using System.Threading.Tasks;
45
using System.Text;
@@ -18,7 +19,7 @@ public static async Task Main(string[] args)
1819
var builder = WebAssemblyHostBuilder.CreateDefault(args);
1920
builder.RootComponents.Add<App>("app");
2021

21-
builder.Services.AddBaseAddressHttpClient();
22+
builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
2223
#if (IndividualLocalAuth)
2324
#if (Hosted)
2425
builder.Services.AddApiAuthorization();

0 commit comments

Comments
 (0)