Skip to content

Commit 1950e59

Browse files
Make static prerendering support auth. Fixes #11799 (#12318)
* Make E2E prerendering test use static prerendering (we no longer need coverage for stateful prerendering) * Use authentication state during static prerendering. This replicates issue #11799 in the E2E test * Initialize the authentication state provider during static prerendering * Update ref assembly * Update unit test
1 parent c25a0d1 commit 1950e59

12 files changed

+143
-103
lines changed

src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,10 @@ public partial interface IHandleEvent
359359
{
360360
System.Threading.Tasks.Task HandleEventAsync(Microsoft.AspNetCore.Components.EventCallbackWorkItem item, object arg);
361361
}
362+
public partial interface IHostEnvironmentAuthenticationStateProvider
363+
{
364+
void SetAuthenticationState(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> authenticationStateTask);
365+
}
362366
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=true)]
363367
public sealed partial class InjectAttribute : System.Attribute
364368
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading.Tasks;
5+
6+
namespace Microsoft.AspNetCore.Components
7+
{
8+
/// <summary>
9+
/// An interface implemented by <see cref="AuthenticationStateProvider"/> classes that can receive authentication
10+
/// state information from the host environment.
11+
/// </summary>
12+
public interface IHostEnvironmentAuthenticationStateProvider
13+
{
14+
/// <summary>
15+
/// Supplies updated authentication state data to the <see cref="AuthenticationStateProvider"/>.
16+
/// </summary>
17+
/// <param name="authenticationStateTask">A task that resolves with the updated <see cref="AuthenticationState"/>.</param>
18+
void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask);
19+
}
20+
}

src/Components/Server/src/Circuits/DefaultCircuitFactory.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
using Microsoft.Extensions.DependencyInjection;
1515
using Microsoft.Extensions.Logging;
1616
using Microsoft.JSInterop;
17+
using System.Threading.Tasks;
1718

1819
namespace Microsoft.AspNetCore.Components.Server.Circuits
1920
{
@@ -50,9 +51,12 @@ public override CircuitHost CreateCircuitHost(
5051
jsRuntime.Initialize(client);
5152
componentContext.Initialize(client);
5253

53-
// You can replace the AuthenticationStateProvider with a custom one, but in that case initialization is up to you
54-
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>();
55-
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User);
54+
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
55+
if (authenticationStateProvider != null)
56+
{
57+
var authenticationState = new AuthenticationState(httpContext.User); // TODO: Get this from the hub connection context instead
58+
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
59+
}
5660

5761
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
5862
var navigationInterception = (RemoteNavigationInterception)scope.ServiceProvider.GetRequiredService<INavigationInterception>();

src/Components/Server/src/Circuits/FixedAuthenticationStateProvider.cs

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Threading.Tasks;
6+
7+
namespace Microsoft.AspNetCore.Components.Server.Circuits
8+
{
9+
/// <summary>
10+
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side Blazor.
11+
/// </summary>
12+
internal class ServerAuthenticationStateProvider : AuthenticationStateProvider, IHostEnvironmentAuthenticationStateProvider
13+
{
14+
private Task<AuthenticationState> _authenticationStateTask;
15+
16+
public override Task<AuthenticationState> GetAuthenticationStateAsync()
17+
=> _authenticationStateTask
18+
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(SetAuthenticationState)}.");
19+
20+
public void SetAuthenticationState(Task<AuthenticationState> authenticationStateTask)
21+
{
22+
_authenticationStateTask = authenticationStateTask ?? throw new ArgumentNullException(nameof(authenticationStateTask));
23+
NotifyAuthenticationStateChanged(_authenticationStateTask);
24+
}
25+
}
26+
}

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
7676
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
7777
services.AddScoped<INavigationInterception, RemoteNavigationInterception>();
7878
services.AddScoped<IComponentContext, RemoteComponentContext>();
79-
services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>();
79+
services.AddScoped<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
8080

8181
services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CircuitOptions>, CircuitOptionsJSInteropDetailedErrorsConfiguration>());
8282

src/Components/Server/test/Circuits/FixedAuthenticationStateProviderTest.cs

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Security.Claims;
6+
using System.Threading.Tasks;
7+
using Microsoft.AspNetCore.Components.Server.Circuits;
8+
using Xunit;
9+
10+
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
11+
{
12+
public class ServerAuthenticationStateProviderTest
13+
{
14+
[Fact]
15+
public async Task CannotProvideAuthenticationStateBeforeInitialization()
16+
{
17+
await Assert.ThrowsAsync<InvalidOperationException>(() =>
18+
new ServerAuthenticationStateProvider()
19+
.GetAuthenticationStateAsync());
20+
}
21+
22+
[Fact]
23+
public async Task SuppliesAuthenticationStateWithFixedUser()
24+
{
25+
// Arrange
26+
var user = new ClaimsPrincipal();
27+
var provider = new ServerAuthenticationStateProvider();
28+
29+
// Act 1
30+
var expectedAuthenticationState1 = new AuthenticationState(user);
31+
provider.SetAuthenticationState(Task.FromResult(expectedAuthenticationState1));
32+
33+
// Assert 1
34+
var actualAuthenticationState1 = await provider.GetAuthenticationStateAsync();
35+
Assert.NotNull(actualAuthenticationState1);
36+
Assert.Same(expectedAuthenticationState1, actualAuthenticationState1);
37+
38+
// Act 2: Show we can update it further
39+
var expectedAuthenticationState2 = new AuthenticationState(user);
40+
provider.SetAuthenticationState(Task.FromResult(expectedAuthenticationState2));
41+
42+
// Assert 2
43+
var actualAuthenticationState2 = await provider.GetAuthenticationStateAsync();
44+
Assert.NotNull(actualAuthenticationState2);
45+
Assert.NotSame(actualAuthenticationState1, actualAuthenticationState2);
46+
Assert.Same(expectedAuthenticationState2, actualAuthenticationState2);
47+
}
48+
}
49+
}

src/Components/test/testassets/BasicTestApp/PrerenderedToInteractiveTransition.razor

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
@using Microsoft.AspNetCore.Components
33
@inject IComponentContext ComponentContext
44

5-
<h1>Hello</h1>
5+
<CascadingAuthenticationState>
6+
<h1>Hello</h1>
67

7-
<p>
8-
Current state:
9-
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
10-
</p>
8+
<p>
9+
Current state:
10+
<strong id="connected-state">@(ComponentContext.IsConnected ? "connected" : "not connected")</strong>
11+
</p>
1112

12-
<p>
13-
Clicks:
14-
<strong id="count">@count</strong>
15-
<button id="increment-count" @onclick="@(() => count++)">Click me</button>
16-
</p>
13+
<p>
14+
Clicks:
15+
<strong id="count">@count</strong>
16+
<button id="increment-count" @onclick="@(() => count++)">Click me</button>
17+
</p>
18+
</CascadingAuthenticationState>
1719

1820
@code {
1921
int count;

src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<base href="~/" />
88
</head>
99
<body>
10-
<app>@(await Html.RenderComponentAsync<TestRouter>())</app>
10+
<app>@(await Html.RenderStaticComponentAsync<TestRouter>())</app>
1111

1212
@*
1313
So that E2E tests can make assertions about both the prerendered and
@@ -19,17 +19,17 @@
1919

2020
<script src="_framework/blazor.server.js" autostart="false"></script>
2121
<script>
22-
// Used by InteropOnInitializationComponent
23-
function setElementValue(element, newValue) {
24-
element.value = newValue;
25-
return element.value;
26-
}
22+
// Used by InteropOnInitializationComponent
23+
function setElementValue(element, newValue) {
24+
element.value = newValue;
25+
return element.value;
26+
}
2727
28-
function start() {
29-
Blazor.start({
30-
logLevel: 1 // LogLevel.Debug
31-
});
32-
}
28+
function start() {
29+
Blazor.start({
30+
logLevel: 1 // LogLevel.Debug
31+
});
32+
}
3333
</script>
3434
</body>
3535
</html>

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Threading.Tasks;
22
using BasicTestApp;
3+
using BasicTestApp.RouterTest;
34
using Microsoft.AspNetCore.Authentication.Cookies;
45
using Microsoft.AspNetCore.Builder;
56
using Microsoft.AspNetCore.Hosting;
@@ -96,7 +97,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
9697
app.UseEndpoints(endpoints =>
9798
{
9899
endpoints.MapFallbackToPage("/PrerenderedHost");
99-
endpoints.MapBlazorHub();
100+
endpoints.MapBlazorHub<TestRouter>(selector: "app");
100101
});
101102
});
102103

src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public async Task<IEnumerable<string>> PrerenderComponentAsync(
3030
HttpContext httpContext,
3131
Type componentType)
3232
{
33-
InitializeUriHelper(httpContext);
33+
InitializeStandardComponentServices(httpContext);
3434
var loggerFactory = (ILoggerFactory)httpContext.RequestServices.GetService(typeof (ILoggerFactory));
3535
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, loggerFactory, _encoder.Encode))
3636
{
@@ -62,15 +62,21 @@ public async Task<IEnumerable<string>> PrerenderComponentAsync(
6262
}
6363
}
6464

65-
private void InitializeUriHelper(HttpContext httpContext)
65+
private void InitializeStandardComponentServices(HttpContext httpContext)
6666
{
67-
// We don't know here if we are dealing with the default HttpUriHelper registered
68-
// by MVC or with the RemoteUriHelper registered by AddComponents.
6967
// This might not be the first component in the request we are rendering, so
70-
// we need to check if we already initialized the uri helper in this request.
68+
// we need to check if we already initialized the services in this request.
7169
if (!_initialized)
7270
{
7371
_initialized = true;
72+
73+
var authenticationStateProvider = httpContext.RequestServices.GetService<AuthenticationStateProvider>() as IHostEnvironmentAuthenticationStateProvider;
74+
if (authenticationStateProvider != null)
75+
{
76+
var authenticationState = new AuthenticationState(httpContext.User);
77+
authenticationStateProvider.SetAuthenticationState(Task.FromResult(authenticationState));
78+
}
79+
7480
var helper = (UriHelperBase)httpContext.RequestServices.GetRequiredService<IUriHelper>();
7581
helper.InitializeState(GetFullUri(httpContext.Request), GetContextBaseUri(httpContext.Request));
7682
}

0 commit comments

Comments
 (0)