Skip to content

API review: RazorComponentResult #47096

Closed
@SteveSandersonMS

Description

@SteveSandersonMS

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.

Metadata

Metadata

Labels

api-approvedAPI was approved in API review, it can be implementedarea-blazorIncludes: Blazor, Razor Componentsfeature-full-stack-web-uiFull stack web UI with Blazor

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions