Description
Background and Motivation
As part of the Blazor United work, allows developers to render a Razor Component from an MVC action or minimal endpoint.
Proposed API
Result:
namespace Microsoft.AspNetCore.Components.Endpoints
{
/// <summary>
/// An <see cref="IResult"/> that renders a Razor Component.
/// </summary>
public class RazorComponentResult<TComponent> : RazorComponentResult where TComponent: IComponent
{
/// <summary>
/// Constructs an instance of <see cref="RazorComponentResult"/>.
/// </summary>
/// <param name="parameters">Parameters for the component.</param>
public RazorComponentResult() {}
public RazorComponentResult(object parameters) {}
public RazorComponentResult(IReadOnlyDictionary<string, object?> parameters) {}
}
/// <summary>
/// An <see cref="IResult"/> that renders a Razor Component.
/// </summary>
public class RazorComponentResult : IResult
{
/// <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) {}
public RazorComponentResult(Type componentType, object? parameters) {}
public RazorComponentResult(Type componentType, IReadOnlyDictionary<string, object?>? 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 a flag to indicate whether streaming rendering should be prevented. If true, the renderer will
/// wait for the component hierarchy to complete asynchronous tasks such as loading before supplying the HTML response.
/// If false, streaming rendering will be determined by the components being rendered.
///
/// The default value is false.
/// </summary>
public bool PreventStreamingRendering { get; set; }
/// <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) {}
}
}
Result executor, which is registered in DI as singleton by default:
namespace Microsoft.AspNetCore.Components.Endpoints
{
/// <summary>
/// Executes a <see cref="RazorComponentResult"/>.
/// </summary>
public class RazorComponentResultExecutor
{
/// <summary>
/// The default content-type header value for Razor Components, <c>text/html; charset=utf-8</c>.
/// </summary>
public static readonly string DefaultContentType = "text/html; charset=utf-8";
/// <summary>
/// Executes a <see cref="RazorComponentResult"/> asynchronously.
/// </summary>
public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult result) {}
}
}
Usage Examples
We may later add helper methods for creating these without having to new
them manually. That is tracked by #47094. However it would also be possible to construct them manually:
Minimal:
endpoints.Map("/mything", () => new RazorComponentResult<MyPageComponent>());
endpoints.Map("/products/{categoryId}", (string categoryId) =>
{
var componentParameters= new { ItemsPerPage = 10, Category = categoryId };
return new RazorComponentResult<MyPageComponent>(componentParameters)
{
StatusCode = 212,
RenderMode = RenderMode.WebAssembly,
ContentType = "text/html",
};
}));
MVC:
public class HomeController : Controller
{
public IResult Home() => new RazorComponentResult<MyPageComponent>();
public IResult Products(string categoryId)
{
var componentParameters = new { ItemsPerPage = 10, Category = categoryId };
return new RazorComponentResult<MyPageComponent>(componentParameters)
{
StatusCode = 212,
RenderMode = RenderMode.WebAssembly,
ContentType = "text/html",
};
}
}
Alternative Designs
We considered a few different ways to support passing parameters to the component, including:
A. As a ParameterView
B. As an anonymously-typed object (like the existing Html.RenderComponentAsync
helper or <component>
tag helper does)
C. As an IReadOnlyDictionary<string, object>
(like the existing Html.RenderComponentAsync
helper or <component>
tag helper does)
D. By actually instantiating the component instance yourself and setting its parameter properties directly
Of these, the ones supported in this PR are B and C, and in both cases it will coerce the value you give to an IReadOnlyDictionary<string, object>
because (1) we have to do that anyway, as it's a step on the route to coercing it to a ParameterView
which the component actually needs, and (2) having it as a dictionary makes it easy for customers' unit tests to read back the contents of Parameters
to make assertions about what their MVC actions are doing.
I haven't put in support for passing a ParameterView
directly because it's unlikely anybody would find that convenient. But we could easily add that constructor overload if we wanted.
I haven't put in support for option D, because it would violate the normal component lifecycle by bypassing the SetParametersAsync
method. Most components would still work fine in practice, but some legitimately-authored components could fail to work if their parameters were not set via SetParametersAsync
. If we were determined to do D, it would probably have to be achieved by using reflection to discover the [Parameter]
properties, reading back the values you set, resetting them back to defaults, then preparing a ParameterView
that contains the values to be assigned via SetParametersAsync
. That's not super lovely because it relies on reflection, and it's always possible that custom getters/setters on the parameter properties would throw if you do something unexpected like explicitly write a null value to them.
Risks
Nothing specific. This is the most basic and obvious way to achieve some functionality we know we want.