Skip to content

Razor Component IResult #47023

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
var requestServices = ViewContext.HttpContext.RequestServices;
var componentPrerenderer = requestServices.GetRequiredService<ComponentPrerenderer>();
var parameters = _parameters is null || _parameters.Count == 0 ? ParameterView.Empty : ParameterView.FromDictionary(_parameters);
var result = await componentPrerenderer.PrerenderComponentAsync(ViewContext, ComponentType, RenderMode, parameters);
var result = await componentPrerenderer.PrerenderComponentAsync(ViewContext.HttpContext, ComponentType, RenderMode, parameters);

// Reset the TagName. We don't want `component` to render.
output.TagName = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu
var renderer = services.GetRequiredService<HtmlRenderer>();
var store = PersistenceMode switch
{
null => ComponentPrerenderer.GetPersistStateRenderMode(ViewContext) switch
null => ComponentPrerenderer.GetPersistStateRenderMode(ViewContext.HttpContext) switch
{
InvokedRenderModes.Mode.None =>
null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public async Task ExecuteAsync_RendersWebAssemblyStateImplicitlyWhenAWebAssembly
ViewContext = GetViewContext()
};

ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered);
ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, RenderMode.WebAssemblyPrerendered);

var context = GetTagHelperContext();
var output = GetTagHelperOutput();
Expand Down Expand Up @@ -128,7 +128,7 @@ public async Task ExecuteAsync_RendersServerStateImplicitlyWhenAServerComponentW
ViewContext = GetViewContext()
};

ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered);
ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, RenderMode.ServerPrerendered);

var context = GetTagHelperContext();
var output = GetTagHelperOutput();
Expand All @@ -153,8 +153,8 @@ public async Task ExecuteAsync_ThrowsIfItCantInferThePersistMode()
ViewContext = GetViewContext()
};

ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.ServerPrerendered);
ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext, RenderMode.WebAssemblyPrerendered);
ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, RenderMode.ServerPrerendered);
ComponentPrerenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, RenderMode.WebAssemblyPrerendered);

var context = GetTagHelperContext();
var output = GetTagHelperOutput();
Expand Down
2 changes: 1 addition & 1 deletion src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public async Task WriteToAsync(TextWriter writer, HtmlEncoder encoder)
continue;
}

if (value.Value is IAsyncHtmlContent valueAsAsyncHtmlContent)
if (value.Value is IHtmlAsyncContent valueAsAsyncHtmlContent)
{
await valueAsAsyncHtmlContent.WriteToAsync(writer);
await writer.FlushAsync();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ internal static void AddViewServices(IServiceCollection services)
services.TryAddScoped<ComponentStatePersistenceManager>();
services.TryAddScoped<PersistentComponentState>(sp => sp.GetRequiredService<ComponentStatePersistenceManager>().State);
services.TryAddScoped<IErrorBoundaryLogger, PrerenderingErrorBoundaryLogger>();
services.TryAddScoped<RazorComponentResultExecutor>();

services.TryAddTransient<ControllerSaveTempDataPropertyFilter>();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
Expand Down
24 changes: 23 additions & 1 deletion src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,24 @@
#nullable enable
Microsoft.AspNetCore.Mvc.Rendering.FormMethod.Dialog = 2 -> Microsoft.AspNetCore.Mvc.Rendering.FormMethod
Microsoft.AspNetCore.Mvc.Rendering.FormMethod.Dialog = 2 -> Microsoft.AspNetCore.Mvc.Rendering.FormMethod
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult<TComponent>.RazorComponentResult() -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext httpContext) -> System.Threading.Tasks.Task
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.Parameters.get -> System.Collections.Generic.IReadOnlyDictionary<string, object>
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RenderMode.get -> Microsoft.AspNetCore.Mvc.Rendering.RenderMode
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RenderMode.set -> void
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.StatusCode.get -> int?
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.StatusCode.set -> void
Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResultExecutor
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.ComponentType.get -> System.Type
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.ContentType.get -> string
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.ContentType.set -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RazorComponentResult(System.Type componentType) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RazorComponentResult(System.Type componentType, object parameters) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RazorComponentResult(System.Type componentType, System.Collections.Generic.IReadOnlyDictionary<string, object> parameters) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult<TComponent>
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult<TComponent>.RazorComponentResult(object parameters) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult<TComponent>.RazorComponentResult(System.Collections.Generic.IReadOnlyDictionary<string, object> parameters) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResultExecutor.RazorComponentResultExecutor(Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory writerFactory) -> void
~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResultExecutor.WriterFactory.get -> Microsoft.AspNetCore.Mvc.Infrastructure.IHttpResponseStreamWriterFactory
~static readonly Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResultExecutor.DefaultContentType -> string
~virtual Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResultExecutor.ExecuteAsync(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult result) -> System.Threading.Tasks.Task
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ public ComponentPrerenderer(

public Dispatcher Dispatcher => _htmlRenderer.Dispatcher;

public async ValueTask<IHtmlContent> PrerenderComponentAsync(
ViewContext viewContext,
public async ValueTask<IHtmlAsyncContent> PrerenderComponentAsync(
HttpContext httpContext,
Type componentType,
RenderMode prerenderMode,
ParameterView parameters)
{
ArgumentNullException.ThrowIfNull(viewContext);
ArgumentNullException.ThrowIfNull(httpContext);
ArgumentNullException.ThrowIfNull(componentType);

if (!typeof(IComponent).IsAssignableFrom(componentType))
Expand All @@ -53,19 +53,18 @@ public async ValueTask<IHtmlContent> PrerenderComponentAsync(
}

// Make sure we only initialize the services once, but on every call we wait for that process to complete
var httpContext = viewContext.HttpContext;
lock (_servicesInitializedLock)
{
_servicesInitializedTask ??= InitializeStandardComponentServicesAsync(httpContext);
}
await _servicesInitializedTask;

UpdateSaveStateRenderMode(viewContext, prerenderMode);
UpdateSaveStateRenderMode(httpContext, prerenderMode);

return prerenderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(httpContext, GetOrCreateInvocationId(viewContext), componentType, parameters),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(httpContext, GetOrCreateInvocationId(viewContext), componentType, parameters),
RenderMode.Server => NonPrerenderedServerComponent(httpContext, GetOrCreateInvocationId(httpContext), componentType, parameters),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(httpContext, GetOrCreateInvocationId(httpContext), componentType, parameters),
RenderMode.Static => await StaticComponentAsync(httpContext, componentType, parameters),
RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(componentType, parameters),
RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(httpContext, componentType, parameters),
Expand Down Expand Up @@ -101,29 +100,30 @@ private async ValueTask<HtmlComponent> PrerenderComponentCoreAsync(
}
}

private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
private static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpContext httpContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
if (!httpContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
httpContext.Items[ComponentSequenceKey] = result;
}

return (ServerComponentInvocationSequence)result;
}

// Internal for test only
internal static void UpdateSaveStateRenderMode(ViewContext viewContext, RenderMode mode)
internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMode mode)
{
// TODO: This will all have to change when we support multiple render modes in the same response
if (mode == RenderMode.ServerPrerendered || mode == RenderMode.WebAssemblyPrerendered)
{
if (!viewContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
{
result = new InvokedRenderModes(mode is RenderMode.ServerPrerendered ?
InvokedRenderModes.Mode.Server :
InvokedRenderModes.Mode.WebAssembly);

viewContext.Items[InvokedRenderModesKey] = result;
httpContext.Items[InvokedRenderModesKey] = result;
}
else
{
Expand All @@ -140,9 +140,9 @@ internal static void UpdateSaveStateRenderMode(ViewContext viewContext, RenderMo
}
}

internal static InvokedRenderModes.Mode GetPersistStateRenderMode(ViewContext viewContext)
internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext)
{
if (viewContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
if (httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result))
{
return ((InvokedRenderModes)result).Value;
}
Expand All @@ -152,7 +152,7 @@ internal static InvokedRenderModes.Mode GetPersistStateRenderMode(ViewContext vi
}
}

private async ValueTask<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
private async ValueTask<IHtmlAsyncContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var htmlComponent = await PrerenderComponentCoreAsync(
parametersCollection,
Expand All @@ -161,7 +161,7 @@ private async ValueTask<IHtmlContent> StaticComponentAsync(HttpContext context,
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, null);
}

private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
private async Task<IHtmlAsyncContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (!context.Response.HasStarted)
{
Expand All @@ -182,7 +182,7 @@ private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext con
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, marker, null);
}

private async ValueTask<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
private async ValueTask<IHtmlAsyncContent> PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var marker = WebAssemblyComponentSerializer.SerializeInvocation(
type,
Expand All @@ -197,7 +197,7 @@ private async ValueTask<IHtmlContent> PrerenderedWebAssemblyComponentAsync(HttpC
return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, marker);
}

private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
private IHtmlAsyncContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (!context.Response.HasStarted)
{
Expand All @@ -208,15 +208,15 @@ private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerCo
return new PrerenderedComponentHtmlContent(null, null, marker, null);
}

private static IHtmlContent NonPrerenderedWebAssemblyComponent(Type type, ParameterView parametersCollection)
private static IHtmlAsyncContent NonPrerenderedWebAssemblyComponent(Type type, ParameterView parametersCollection)
{
var marker = WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false);
return new PrerenderedComponentHtmlContent(null, null, null, marker);
}

// An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response.
// We don't construct the actual HTML until we receive the call to WriteTo.
private class PrerenderedComponentHtmlContent : IHtmlContent, IAsyncHtmlContent
private class PrerenderedComponentHtmlContent : IHtmlContent, IHtmlAsyncContent
{
private readonly Dispatcher _dispatcher;
private readonly HtmlComponent _htmlToEmitOrNull;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Licensed to the .NET Foundation under one or more agreements.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Html;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures;

// For prerendered components, we can't use IHtmlComponent directly because it has no asynchrony and
// hence can't dispatch to the renderer's sync context.
internal interface IAsyncHtmlContent
internal interface IHtmlAsyncContent : IHtmlContent
{
ValueTask WriteToAsync(TextWriter writer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;

namespace Microsoft.AspNetCore.Mvc.ViewFeatures;

/// <summary>
/// An <see cref="IResult"/> that renders a Razor Component.
/// </summary>
public class RazorComponentResult : IResult
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we have changed this to only implement IResult and not IActionResult, does it run through the MVC result filters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, MVC runs result filters around IResult results.

{
private static readonly IReadOnlyDictionary<string, object> EmptyParameters
= new Dictionary<string, object>().AsReadOnly();

/// <summary>
/// Constructs an instance of <see cref="RazorComponentResult"/>.
/// </summary>
/// <param name="componentType">The type of the component to render. This must implement <see cref="IComponent"/>.</param>
public RazorComponentResult(Type componentType)
: this(componentType, null)
{
}

/// <summary>
/// Constructs an instance of <see cref="RazorComponentResult"/>.
/// </summary>
/// <param name="componentType">The type of the component to render. This must implement <see cref="IComponent"/>.</param>
/// <param name="parameters">Parameters for the component.</param>
public RazorComponentResult(Type componentType, object parameters)
: this(componentType, CoerceParametersObjectToDictionary(parameters))
{
}

/// <summary>
/// Constructs an instance of <see cref="RazorComponentResult"/>.
/// </summary>
/// <param name="componentType">The type of the component to render. This must implement <see cref="IComponent"/>.</param>
/// <param name="parameters">Parameters for the component.</param>
public RazorComponentResult(Type componentType, IReadOnlyDictionary<string, object> parameters)
{
// Note that the Blazor renderer will validate that componentType implements IComponent and throws a suitable
// exception if not, so we don't need to duplicate that logic here.

ArgumentNullException.ThrowIfNull(componentType);
ComponentType = componentType;
Parameters = parameters ?? EmptyParameters;
}

private static IReadOnlyDictionary<string, object> CoerceParametersObjectToDictionary(object parameters)
=> parameters is null
? null
: (IReadOnlyDictionary<string, object>)PropertyHelper.ObjectToDictionary(parameters);

/// <summary>
/// Gets the component type.
/// </summary>
public Type ComponentType { get; }

/// <summary>
/// Gets or sets the Content-Type header for the response.
/// </summary>
public string ContentType { get; set; }

/// <summary>
/// Gets or sets the HTTP status code.
/// </summary>
public int? StatusCode { get; set; }

/// <summary>
/// Gets the parameters for the component.
/// </summary>
public IReadOnlyDictionary<string, object> Parameters { get; }

/// <summary>
/// Gets or sets the rendering mode.
/// </summary>
public RenderMode RenderMode { get; set; } = RenderMode.Static;

/// <summary>
/// Requests the service of
/// <see cref="RazorComponentResultExecutor.ExecuteAsync(HttpContext, RazorComponentResult)" />
/// to process itself in the given <paramref name="httpContext" />.
/// </summary>
/// <param name="httpContext">An <see cref="HttpContext" /> associated with the current request.</param >
/// <returns >A <see cref="T:System.Threading.Tasks.Task" /> which will complete when execution is completed.</returns >
public Task ExecuteAsync(HttpContext httpContext)
{
var executor = httpContext.RequestServices.GetRequiredService<RazorComponentResultExecutor>();
return executor.ExecuteAsync(httpContext, this);
}
}
Loading