Support navigation during prerendering (#10694)
This commit is contained in:
parent
15b7dc5aee
commit
1165a6fb16
|
|
@ -361,6 +361,11 @@ namespace Microsoft.AspNetCore.Components
|
||||||
public static explicit operator Microsoft.AspNetCore.Components.MarkupString (string value) { throw null; }
|
public static explicit operator Microsoft.AspNetCore.Components.MarkupString (string value) { throw null; }
|
||||||
public override string ToString() { throw null; }
|
public override string ToString() { throw null; }
|
||||||
}
|
}
|
||||||
|
public partial class NavigationException : System.Exception
|
||||||
|
{
|
||||||
|
public NavigationException(string uri) { }
|
||||||
|
public string Location { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
|
}
|
||||||
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
|
||||||
public readonly partial struct Parameter
|
public readonly partial struct Parameter
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception thrown when an <see cref="IUriHelper"/> is not able to navigate to a different url.
|
||||||
|
/// </summary>
|
||||||
|
public class NavigationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new <see cref="NavigationException"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
public NavigationException(string uri)
|
||||||
|
{
|
||||||
|
Location = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the uri to which navigation was attempted.
|
||||||
|
/// </summary>
|
||||||
|
public string Location { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,14 +5,17 @@ using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.ExceptionServices;
|
using System.Runtime.ExceptionServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
{
|
{
|
||||||
internal class CircuitPrerenderer : IComponentPrerenderer
|
internal class CircuitPrerenderer : IComponentPrerenderer
|
||||||
{
|
{
|
||||||
private static object CircuitHostKey = new object();
|
private static object CircuitHostKey = new object();
|
||||||
|
private static object NavigationStatusKey = new object();
|
||||||
|
|
||||||
private readonly CircuitFactory _circuitFactory;
|
private readonly CircuitFactory _circuitFactory;
|
||||||
private readonly CircuitRegistry _registry;
|
private readonly CircuitRegistry _registry;
|
||||||
|
|
@ -26,11 +29,41 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
public async Task<ComponentPrerenderResult> PrerenderComponentAsync(ComponentPrerenderingContext prerenderingContext)
|
||||||
{
|
{
|
||||||
var context = prerenderingContext.Context;
|
var context = prerenderingContext.Context;
|
||||||
var circuitHost = GetOrCreateCircuitHost(context);
|
var navigationStatus = GetOrCreateNavigationStatus(context);
|
||||||
|
if (navigationStatus.Navigated)
|
||||||
|
{
|
||||||
|
// Avoid creating a circuit host if other component earlier in the pipeline already triggered
|
||||||
|
// a navigation request. Instead rendre nothing
|
||||||
|
return new ComponentPrerenderResult(Array.Empty<string>());
|
||||||
|
}
|
||||||
|
var circuitHost = GetOrCreateCircuitHost(context, navigationStatus);
|
||||||
|
ComponentRenderedText renderResult = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
renderResult = await circuitHost.PrerenderComponentAsync(
|
||||||
|
prerenderingContext.ComponentType,
|
||||||
|
prerenderingContext.Parameters);
|
||||||
|
}
|
||||||
|
catch (NavigationException navigationException)
|
||||||
|
{
|
||||||
|
// Cleanup the state as we won't need it any longer.
|
||||||
|
// Signal callbacks that we don't have to register the circuit.
|
||||||
|
await CleanupCircuitState(context, navigationStatus, circuitHost);
|
||||||
|
|
||||||
var renderResult = await circuitHost.PrerenderComponentAsync(
|
// Navigation was attempted during prerendering.
|
||||||
prerenderingContext.ComponentType,
|
if (prerenderingContext.Context.Response.HasStarted)
|
||||||
prerenderingContext.Parameters);
|
{
|
||||||
|
// We can't perform a redirect as the server already started sending the response.
|
||||||
|
// This is considered an application error as the developer should buffer the response until
|
||||||
|
// all components have rendered.
|
||||||
|
throw new InvalidOperationException("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
||||||
|
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
||||||
|
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", navigationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.Redirect(navigationException.Location);
|
||||||
|
return new ComponentPrerenderResult(Array.Empty<string>());
|
||||||
|
}
|
||||||
|
|
||||||
circuitHost.Descriptors.Add(new ComponentDescriptor
|
circuitHost.Descriptors.Add(new ComponentDescriptor
|
||||||
{
|
{
|
||||||
|
|
@ -38,9 +71,9 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
Prerendered = true
|
Prerendered = true
|
||||||
});
|
});
|
||||||
|
|
||||||
var result = new[] {
|
var result = (new[] {
|
||||||
$"<!-- M.A.C.Component:{{\"circuitId\":\"{circuitHost.CircuitId}\",\"rendererId\":\"{circuitHost.Renderer.Id}\",\"componentId\":\"{renderResult.ComponentId}\"}} -->",
|
$"<!-- M.A.C.Component:{{\"circuitId\":\"{circuitHost.CircuitId}\",\"rendererId\":\"{circuitHost.Renderer.Id}\",\"componentId\":\"{renderResult.ComponentId}\"}} -->",
|
||||||
}.Concat(renderResult.Tokens).Concat(
|
}).Concat(renderResult.Tokens).Concat(
|
||||||
new[] {
|
new[] {
|
||||||
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
|
$"<!-- M.A.C.Component: {renderResult.ComponentId} -->"
|
||||||
});
|
});
|
||||||
|
|
@ -48,7 +81,28 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return new ComponentPrerenderResult(result);
|
return new ComponentPrerenderResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CircuitHost GetOrCreateCircuitHost(HttpContext context)
|
private CircuitNavigationStatus GetOrCreateNavigationStatus(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Items.TryGetValue(NavigationStatusKey, out var existingHost))
|
||||||
|
{
|
||||||
|
return (CircuitNavigationStatus)existingHost;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var navigationStatus = new CircuitNavigationStatus();
|
||||||
|
context.Items[NavigationStatusKey] = navigationStatus;
|
||||||
|
return navigationStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task CleanupCircuitState(HttpContext context, CircuitNavigationStatus navigationStatus, CircuitHost circuitHost)
|
||||||
|
{
|
||||||
|
navigationStatus.Navigated = true;
|
||||||
|
context.Items.Remove(CircuitHostKey);
|
||||||
|
await circuitHost.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CircuitHost GetOrCreateCircuitHost(HttpContext context, CircuitNavigationStatus navigationStatus)
|
||||||
{
|
{
|
||||||
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
|
if (context.Items.TryGetValue(CircuitHostKey, out var existingHost))
|
||||||
{
|
{
|
||||||
|
|
@ -66,7 +120,11 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
context.Response.OnCompleted(() =>
|
context.Response.OnCompleted(() =>
|
||||||
{
|
{
|
||||||
result.UnhandledException -= CircuitHost_UnhandledException;
|
result.UnhandledException -= CircuitHost_UnhandledException;
|
||||||
_registry.RegisterDisconnectedCircuit(result);
|
if (!navigationStatus.Navigated)
|
||||||
|
{
|
||||||
|
_registry.RegisterDisconnectedCircuit(result);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
});
|
});
|
||||||
context.Items.Add(CircuitHostKey, result);
|
context.Items.Add(CircuitHostKey, result);
|
||||||
|
|
@ -105,5 +163,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class CircuitNavigationStatus
|
||||||
|
{
|
||||||
|
public bool Navigated { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
||||||
if (_jsRuntime == null)
|
if (_jsRuntime == null)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException("Navigation commands can not be issued at this time. This is because the component is being " +
|
throw new NavigationException(uri);
|
||||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
|
||||||
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
|
|
||||||
"attempted during prerendering or while the client is disconnected.");
|
|
||||||
}
|
}
|
||||||
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
|
_jsRuntime.InvokeAsync<object>(Interop.NavigateTo, uri, forceLoad);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,11 @@ using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
using Microsoft.AspNetCore.Html;
|
using Microsoft.AspNetCore.Html;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -122,7 +124,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UriHelperRedirect_ThrowsInvalidOperationException()
|
public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponseHasAlreadyStarted()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var ctx = new DefaultHttpContext();
|
var ctx = new DefaultHttpContext();
|
||||||
|
|
@ -131,7 +133,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
ctx.Request.PathBase = "/base";
|
ctx.Request.PathBase = "/base";
|
||||||
ctx.Request.Path = "/path";
|
ctx.Request.Path = "/path";
|
||||||
ctx.Request.QueryString = new QueryString("?query=value");
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
|
var responseMock = new Mock<IHttpResponseFeature>();
|
||||||
|
responseMock.Setup(r => r.HasStarted).Returns(true);
|
||||||
|
ctx.Features.Set(responseMock.Object);
|
||||||
var helper = CreateHelper(ctx);
|
var helper = CreateHelper(ctx);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
|
@ -141,13 +145,62 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
RedirectUri = "http://localhost/redirect"
|
RedirectUri = "http://localhost/redirect"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.Equal("Navigation commands can not be issued at this time. This is because the component is being " +
|
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
||||||
"prerendered and the page has not yet loaded in the browser or because the circuit is currently disconnected. " +
|
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
||||||
"Components must wrap any navigation calls in conditional logic to ensure those navigation calls are not " +
|
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
|
||||||
"attempted during prerendering or while the client is disconnected.",
|
|
||||||
exception.Message);
|
exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HtmlHelper_Redirects_WhenComponentNavigates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ctx = new DefaultHttpContext();
|
||||||
|
ctx.Request.Scheme = "http";
|
||||||
|
ctx.Request.Host = new HostString("localhost");
|
||||||
|
ctx.Request.PathBase = "/base";
|
||||||
|
ctx.Request.Path = "/path";
|
||||||
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
|
var helper = CreateHelper(ctx);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await helper.RenderComponentAsync<RedirectComponent>(new
|
||||||
|
{
|
||||||
|
RedirectUri = "http://localhost/redirect"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(302, ctx.Response.StatusCode);
|
||||||
|
Assert.Equal("http://localhost/redirect", ctx.Response.Headers[HeaderNames.Location]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HtmlHelper_AvoidsRendering_WhenNavigationHasHappened()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ctx = new DefaultHttpContext();
|
||||||
|
ctx.Request.Scheme = "http";
|
||||||
|
ctx.Request.Host = new HostString("localhost");
|
||||||
|
ctx.Request.PathBase = "/base";
|
||||||
|
ctx.Request.Path = "/path";
|
||||||
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
|
var helper = CreateHelper(ctx);
|
||||||
|
var stringWriter = new StringWriter();
|
||||||
|
|
||||||
|
await helper.RenderComponentAsync<RedirectComponent>(new
|
||||||
|
{
|
||||||
|
RedirectUri = "http://localhost/redirect"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await helper.RenderComponentAsync<GreetingComponent>(new { Name = "George" });
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(result);
|
||||||
|
result.WriteTo(stringWriter, HtmlEncoder.Default);
|
||||||
|
Assert.Equal("", stringWriter.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanRender_AsyncComponent()
|
public async Task CanRender_AsyncComponent()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -10,10 +10,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
{
|
{
|
||||||
protected override void NavigateToCore(string uri, bool forceLoad)
|
protected override void NavigateToCore(string uri, bool forceLoad)
|
||||||
{
|
{
|
||||||
// For now throw as we don't have a good way of aborting the request from here.
|
throw new NavigationException(uri);
|
||||||
throw new InvalidOperationException("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
|
|
||||||
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
|
|
||||||
"attempted during prerendering.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Extensions;
|
using Microsoft.AspNetCore.Http.Extensions;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
|
||||||
|
|
@ -33,9 +34,30 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
|
||||||
InitializeUriHelper(httpContext);
|
InitializeUriHelper(httpContext);
|
||||||
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, _encoder.Encode, dispatcher))
|
using (var htmlRenderer = new HtmlRenderer(httpContext.RequestServices, _encoder.Encode, dispatcher))
|
||||||
{
|
{
|
||||||
var result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
|
ComponentRenderedText result = default;
|
||||||
componentType,
|
try
|
||||||
parameters));
|
{
|
||||||
|
result = await dispatcher.InvokeAsync(() => htmlRenderer.RenderComponentAsync(
|
||||||
|
componentType,
|
||||||
|
parameters));
|
||||||
|
}
|
||||||
|
catch (NavigationException navigationException)
|
||||||
|
{
|
||||||
|
// Navigation was attempted during prerendering.
|
||||||
|
if (httpContext.Response.HasStarted)
|
||||||
|
{
|
||||||
|
// We can't perform a redirect as the server already started sending the response.
|
||||||
|
// This is considered an application error as the developer should buffer the response until
|
||||||
|
// all components have rendered.
|
||||||
|
throw new InvalidOperationException("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
||||||
|
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
||||||
|
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", navigationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
httpContext.Response.Redirect(navigationException.Location);
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
return result.Tokens;
|
return result.Tokens;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
using Microsoft.AspNetCore.Components.Server;
|
using Microsoft.AspNetCore.Components.Server;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -107,7 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task UriHelperRedirect_ThrowsInvalidOperationException()
|
public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponseHasAlreadyStarted()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var ctx = new DefaultHttpContext();
|
var ctx = new DefaultHttpContext();
|
||||||
|
|
@ -116,7 +118,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
ctx.Request.PathBase = "/base";
|
ctx.Request.PathBase = "/base";
|
||||||
ctx.Request.Path = "/path";
|
ctx.Request.Path = "/path";
|
||||||
ctx.Request.QueryString = new QueryString("?query=value");
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
|
var responseMock = new Mock<IHttpResponseFeature>();
|
||||||
|
responseMock.Setup(r => r.HasStarted).Returns(true);
|
||||||
|
ctx.Features.Set(responseMock.Object);
|
||||||
var helper = CreateHelper(ctx);
|
var helper = CreateHelper(ctx);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
|
||||||
|
|
@ -125,13 +129,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
{
|
{
|
||||||
RedirectUri = "http://localhost/redirect"
|
RedirectUri = "http://localhost/redirect"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Assert.Equal("Navigation commands can not be issued during server-side prerendering because the page has not yet loaded in the browser" +
|
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
|
||||||
"Components must wrap any navigation commands in conditional logic to ensure those navigation calls are not " +
|
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
|
||||||
"attempted during prerendering.",
|
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
|
||||||
exception.Message);
|
exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task HtmlHelper_Redirects_WhenComponentNavigates()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var ctx = new DefaultHttpContext();
|
||||||
|
ctx.Request.Scheme = "http";
|
||||||
|
ctx.Request.Host = new HostString("localhost");
|
||||||
|
ctx.Request.PathBase = "/base";
|
||||||
|
ctx.Request.Path = "/path";
|
||||||
|
ctx.Request.QueryString = new QueryString("?query=value");
|
||||||
|
var helper = CreateHelper(ctx);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await helper.RenderStaticComponentAsync<RedirectComponent>(new
|
||||||
|
{
|
||||||
|
RedirectUri = "http://localhost/redirect"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(302, ctx.Response.StatusCode);
|
||||||
|
Assert.Equal("http://localhost/redirect", ctx.Response.Headers[HeaderNames.Location]);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task CanRender_AsyncComponent()
|
public async Task CanRender_AsyncComponent()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,37 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
|
AssertComponent("\nRouter component\n<p>Routed successfully</p>", "Routing", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Redirects_Navigation_Component()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var fixture = Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()));
|
||||||
|
fixture.ClientOptions.AllowAutoRedirect = false;
|
||||||
|
var client = CreateClient(fixture);
|
||||||
|
|
||||||
|
var response = await client.GetAsync("http://localhost/components/Navigation");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.Redirect);
|
||||||
|
Assert.Equal("/navigation-redirect", response.Headers.Location.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Redirects_Navigation_ComponentInteractive()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var fixture = Factory.WithWebHostBuilder(builder => builder.ConfigureServices(services => services.AddServerSideBlazor()));
|
||||||
|
fixture.ClientOptions.AllowAutoRedirect = false;
|
||||||
|
var client = CreateClient(fixture);
|
||||||
|
|
||||||
|
var response = await client.GetAsync("http://localhost/components/Navigation/false");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// Assert
|
||||||
|
await response.AssertStatusCodeAsync(HttpStatusCode.Redirect);
|
||||||
|
Assert.Equal("/navigation-redirect", response.Headers.Location.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer()
|
public async Task Renders_RoutingComponent_UsingRazorComponents_Prerenderer()
|
||||||
{
|
{
|
||||||
|
|
@ -224,7 +255,8 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpClient CreateClient(WebApplicationFactory<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
private HttpClient CreateClient(
|
||||||
|
WebApplicationFactory<BasicWebSite.StartupWithoutEndpointRouting> fixture)
|
||||||
{
|
{
|
||||||
var loopHandler = new LoopHttpHandler();
|
var loopHandler = new LoopHttpHandler();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,12 @@ namespace BasicWebSite.Controllers
|
||||||
return Ok(_weatherData);
|
return Ok(_weatherData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("/components/Navigation/{staticPrerender=true}")]
|
||||||
|
public IActionResult Navigation(bool staticPrerender)
|
||||||
|
{
|
||||||
|
return View(staticPrerender);
|
||||||
|
}
|
||||||
|
|
||||||
private class WeatherRow
|
private class WeatherRow
|
||||||
{
|
{
|
||||||
public string DateFormatted { get; internal set; }
|
public string DateFormatted { get; internal set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
@inject IUriHelper Helper
|
||||||
|
|
||||||
|
@functions{
|
||||||
|
|
||||||
|
protected override void OnInit()
|
||||||
|
{
|
||||||
|
Helper.NavigateTo("/navigation-redirect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
@using BasicWebSite.RazorComponents;
|
||||||
|
@model bool;
|
||||||
|
<h1>Navigation components</h1>
|
||||||
|
<div id="Navigation">
|
||||||
|
@if (Model)
|
||||||
|
{
|
||||||
|
@(await Html.RenderStaticComponentAsync<NavigationComponent>())
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@(await Html.RenderComponentAsync<NavigationComponent>())
|
||||||
|
}
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue