diff --git a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs index 448c6d2c9875..b72d354f08c4 100644 --- a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs @@ -94,7 +94,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu var requestServices = ViewContext.HttpContext.RequestServices; var componentPrerenderer = requestServices.GetRequiredService(); 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; diff --git a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs index bf832e1aaa0f..99dac7e83ac1 100644 --- a/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs +++ b/src/Mvc/Mvc.TagHelpers/src/PersistComponentStateTagHelper.cs @@ -52,7 +52,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu var renderer = services.GetRequiredService(); var store = PersistenceMode switch { - null => ComponentPrerenderer.GetPersistStateRenderMode(ViewContext) switch + null => ComponentPrerenderer.GetPersistStateRenderMode(ViewContext.HttpContext) switch { InvokedRenderModes.Mode.None => null, diff --git a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs index 12dcff2797e9..34853e13cf17 100644 --- a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs @@ -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(); @@ -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(); @@ -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(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBuffer.cs b/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBuffer.cs index e082d16b0558..ebcd7a0c934d 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBuffer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Buffers/ViewBuffer.cs @@ -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(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index d3ed50ef8b96..c78eab07e05b 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -212,6 +212,7 @@ internal static void AddViewServices(IServiceCollection services) services.TryAddScoped(); services.TryAddScoped(sp => sp.GetRequiredService().State); services.TryAddScoped(); + services.TryAddScoped(); services.TryAddTransient(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/MvcViewFeaturesDiagnosticListenerExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/MvcViewFeaturesDiagnosticListenerExtensions.cs index 755869805a41..b456ac30fddf 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/MvcViewFeaturesDiagnosticListenerExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/MvcViewFeaturesDiagnosticListenerExtensions.cs @@ -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; diff --git a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt index b7b64d0c74cc..5f27b8d359b5 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.ViewFeatures/src/PublicAPI.Unshipped.txt @@ -1,2 +1,24 @@ #nullable enable -Microsoft.AspNetCore.Mvc.Rendering.FormMethod.Dialog = 2 -> Microsoft.AspNetCore.Mvc.Rendering.FormMethod \ No newline at end of file +Microsoft.AspNetCore.Mvc.Rendering.FormMethod.Dialog = 2 -> Microsoft.AspNetCore.Mvc.Rendering.FormMethod +Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult +Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.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 +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 parameters) -> void +~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult +~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RazorComponentResult(object parameters) -> void +~Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponentResult.RazorComponentResult(System.Collections.Generic.IReadOnlyDictionary 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 diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs index cac3ca8fdf0e..d490e9c54ba4 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentPrerenderer.cs @@ -38,13 +38,13 @@ public ComponentPrerenderer( public Dispatcher Dispatcher => _htmlRenderer.Dispatcher; - public async ValueTask PrerenderComponentAsync( - ViewContext viewContext, + public async ValueTask PrerenderComponentAsync( + HttpContext httpContext, Type componentType, RenderMode prerenderMode, ParameterView parameters) { - ArgumentNullException.ThrowIfNull(viewContext); + ArgumentNullException.ThrowIfNull(httpContext); ArgumentNullException.ThrowIfNull(componentType); if (!typeof(IComponent).IsAssignableFrom(componentType)) @@ -53,19 +53,18 @@ public async ValueTask 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), @@ -101,29 +100,30 @@ private async ValueTask 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 { @@ -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; } @@ -152,7 +152,7 @@ internal static InvokedRenderModes.Mode GetPersistStateRenderMode(ViewContext vi } } - private async ValueTask StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) + private async ValueTask StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) { var htmlComponent = await PrerenderComponentCoreAsync( parametersCollection, @@ -161,7 +161,7 @@ private async ValueTask StaticComponentAsync(HttpContext context, return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, null, null); } - private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) + private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) { if (!context.Response.HasStarted) { @@ -182,7 +182,7 @@ private async Task PrerenderedServerComponentAsync(HttpContext con return new PrerenderedComponentHtmlContent(_htmlRenderer.Dispatcher, htmlComponent, marker, null); } - private async ValueTask PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) + private async ValueTask PrerenderedWebAssemblyComponentAsync(HttpContext context, Type type, ParameterView parametersCollection) { var marker = WebAssemblyComponentSerializer.SerializeInvocation( type, @@ -197,7 +197,7 @@ private async ValueTask 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) { @@ -208,7 +208,7 @@ 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); @@ -216,7 +216,7 @@ private static IHtmlContent NonPrerenderedWebAssemblyComponent(Type type, Parame // 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; diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IAsyncHtmlContent.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IHtmlAsyncContent.cs similarity index 68% rename from src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IAsyncHtmlContent.cs rename to src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IHtmlAsyncContent.cs index faaff366f841..679c9b0c4b9e 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IAsyncHtmlContent.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IHtmlAsyncContent.cs @@ -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); } diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResult.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResult.cs new file mode 100644 index 000000000000..87a0c1764890 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResult.cs @@ -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; + +/// +/// An that renders a Razor Component. +/// +public class RazorComponentResult : IResult +{ + private static readonly IReadOnlyDictionary EmptyParameters + = new Dictionary().AsReadOnly(); + + /// + /// Constructs an instance of . + /// + /// The type of the component to render. This must implement . + public RazorComponentResult(Type componentType) + : this(componentType, null) + { + } + + /// + /// Constructs an instance of . + /// + /// The type of the component to render. This must implement . + /// Parameters for the component. + public RazorComponentResult(Type componentType, object parameters) + : this(componentType, CoerceParametersObjectToDictionary(parameters)) + { + } + + /// + /// Constructs an instance of . + /// + /// The type of the component to render. This must implement . + /// Parameters for the component. + public RazorComponentResult(Type componentType, IReadOnlyDictionary 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 CoerceParametersObjectToDictionary(object parameters) + => parameters is null + ? null + : (IReadOnlyDictionary)PropertyHelper.ObjectToDictionary(parameters); + + /// + /// Gets the component type. + /// + public Type ComponentType { get; } + + /// + /// Gets or sets the Content-Type header for the response. + /// + public string ContentType { get; set; } + + /// + /// Gets or sets the HTTP status code. + /// + public int? StatusCode { get; set; } + + /// + /// Gets the parameters for the component. + /// + public IReadOnlyDictionary Parameters { get; } + + /// + /// Gets or sets the rendering mode. + /// + public RenderMode RenderMode { get; set; } = RenderMode.Static; + + /// + /// Requests the service of + /// + /// to process itself in the given . + /// + /// An associated with the current request. + /// A which will complete when execution is completed. + public Task ExecuteAsync(HttpContext httpContext) + { + var executor = httpContext.RequestServices.GetRequiredService(); + return executor.ExecuteAsync(httpContext, this); + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultExecutor.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultExecutor.cs new file mode 100644 index 000000000000..70c0a3a74e91 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultExecutor.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures; + +/// +/// Executes a Razor Component. +/// +public class RazorComponentResultExecutor +{ + /// + /// The default content-type header value for Razor Components, text/html; charset=utf-8. + /// + public static readonly string DefaultContentType = "text/html; charset=utf-8"; + + /// + /// Constructs an instance of . + /// + /// The . + public RazorComponentResultExecutor( + IHttpResponseStreamWriterFactory writerFactory) + { + ArgumentNullException.ThrowIfNull(writerFactory); + WriterFactory = writerFactory; + } + + /// + /// Gets the . + /// + protected IHttpResponseStreamWriterFactory WriterFactory { get; } + + /// + /// Executes a Razor Component asynchronously. + /// + public virtual async Task ExecuteAsync(HttpContext httpContext, RazorComponentResult result) + { + ArgumentNullException.ThrowIfNull(httpContext); + + var response = httpContext.Response; + + ResponseContentTypeHelper.ResolveContentTypeAndEncoding( + result.ContentType, + response.ContentType, + (DefaultContentType, Encoding.UTF8), + MediaType.GetEncoding, + out var resolvedContentType, + out var resolvedContentTypeEncoding); + + response.ContentType = resolvedContentType; + + if (result.StatusCode != null) + { + response.StatusCode = result.StatusCode.Value; + } + + await using var writer = WriterFactory.CreateWriter(response.Body, resolvedContentTypeEncoding); + await RenderComponentToResponse(httpContext, result, writer); + + // Perf: Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying + // response asynchronously. In the absence of this line, the buffer gets synchronously written to the + // response as part of the Dispose which has a perf impact. + await writer.FlushAsync(); + } + + private static Task RenderComponentToResponse(HttpContext httpContext, RazorComponentResult result, TextWriter writer) + { + var componentPrerenderer = httpContext.RequestServices.GetRequiredService(); + return componentPrerenderer.Dispatcher.InvokeAsync(async () => + { + // We could pool these dictionary instances if we wanted. We could even skip the dictionary + // phase entirely if we added some new ParameterView.FromSingleValue(name, value) API. + var hostParameters = ParameterView.FromDictionary(new Dictionary + { + { nameof(RazorComponentResultHost.RazorComponentResult), result }, + }); + + // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, + // because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host + // component takes care of switching into your desired render mode when it produces its own output. + var htmlContent = await componentPrerenderer.PrerenderComponentAsync( + httpContext, + typeof(RazorComponentResultHost), + RenderMode.Static, + hostParameters); + await htmlContent.WriteToAsync(writer); + }); + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultHost.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultHost.cs new file mode 100644 index 000000000000..d26f88beccd0 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultHost.cs @@ -0,0 +1,92 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures; + +/// +/// Internal component type that acts as a root component when executing a RazorComponentResult. It takes +/// care of rendering the component inside its hierarchy of layouts (via LayoutView) as well as converting +/// any information we want from RazorComponentResult into component parameters. We could also use this to +/// supply other data from the original HttpContext as component parameters, e.g., for model binding. +/// +/// It happens to be almost the same as RouteView except it doesn't supply any query parameters. We can +/// resolve that at the same time we implement support for form posts. +/// +internal class RazorComponentResultHost : IComponent +{ + private RenderHandle _renderHandle; + + public RazorComponentResult RazorComponentResult { get; private set; } + + public void Attach(RenderHandle renderHandle) + => _renderHandle = renderHandle; + + public Task SetParametersAsync(ParameterView parameters) + { + foreach (var kvp in parameters) + { + switch (kvp.Name) + { + case nameof(RazorComponentResult): + RazorComponentResult = (RazorComponentResult)kvp.Value; + break; + default: + throw new ArgumentException($"Unknown parameter '{kvp.Name}'"); + } + } + + if (RazorComponentResult is null) + { + throw new InvalidOperationException($"Failed to supply nonnull value for required parameter '{nameof(RazorComponentResult)}'"); + } + + _renderHandle.Render(BuildRenderTree); + return Task.CompletedTask; + } + + private void BuildRenderTree(RenderTreeBuilder builder) + { + var pageLayoutType = RazorComponentResult.ComponentType.GetCustomAttribute()?.LayoutType; + + builder.OpenComponent(0); + builder.AddComponentParameter(1, nameof(LayoutView.Layout), pageLayoutType); + builder.AddComponentParameter(2, nameof(LayoutView.ChildContent), (RenderFragment)RenderPageWithParameters); + builder.CloseComponent(); + } + + private void RenderPageWithParameters(RenderTreeBuilder builder) + { + // TODO: Once we support rendering Server/WebAssembly components into the page, implementation will + // go here. We need to switch into the rendermode given by RazorComponentResult.RenderMode for this + // child component. That will cause the developer-supplied parameters to be serialized into a marker + // but not attempt to serialize the RenderFragment that causes this to be hosted in its layout. + if (RazorComponentResult.RenderMode != RenderMode.Static) + { + // Tracked by #46353 and #46354 + throw new NotSupportedException($"Currently, {nameof(RazorComponentResult)} only supports the {RenderMode.Static} render mode."); + } + + // TODO: If we wanted to support rendering pre-instantiated component objects, it would probably go here + // as either a whole new RenderTreeFrame type or as some mechanism to attach the instance onto a regular + // Component frame (perhaps using a dummy ComponentState that represents 'not yet initialized') + + builder.OpenComponent(0, RazorComponentResult.ComponentType); + + if (RazorComponentResult.Parameters is not null) + { + var dict = PropertyHelper.ObjectToDictionary(RazorComponentResult.Parameters); + foreach (var kvp in dict) + { + builder.AddComponentParameter(1, kvp.Key, kvp.Value); + } + } + + builder.CloseComponent(); + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultOfT.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultOfT.cs new file mode 100644 index 000000000000..525b491145f5 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/RazorComponentResultOfT.cs @@ -0,0 +1,36 @@ +// 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; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures; + +/// +/// An that renders a Razor Component. +/// +public class RazorComponentResult : RazorComponentResult where TComponent: IComponent +{ + /// + /// Constructs an instance of . + /// + public RazorComponentResult() : base(typeof(TComponent)) + { + } + + /// + /// Constructs an instance of . + /// + /// Parameters for the component. + public RazorComponentResult(object parameters) : base(typeof(TComponent), parameters) + { + } + + /// + /// Constructs an instance of . + /// + /// Parameters for the component. + public RazorComponentResult(IReadOnlyDictionary parameters) : base(typeof(TComponent), parameters) + { + } +} diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs index a6f3078d2b09..f75e058525e3 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs @@ -57,8 +57,8 @@ public static async Task RenderComponentAsync( ParameterView.Empty : ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters)); - var viewContext = htmlHelper.ViewContext; - var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService(); - return await componentRenderer.PrerenderComponentAsync(viewContext, componentType, renderMode, parameterView); + var httpContext = htmlHelper.ViewContext.HttpContext; + var componentRenderer = httpContext.RequestServices.GetRequiredService(); + return await componentRenderer.PrerenderComponentAsync(httpContext, componentType, renderMode, parameterView); } } diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs index e644578d0260..9d285bce0f4a 100644 --- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs +++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentPrerendererTest.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.Rendering; -using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -43,11 +42,11 @@ public ComponentPrerendererTest() public async Task CanRender_ParameterlessComponent_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssembly, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.WebAssembly, ParameterView.Empty); result.WriteTo(writer, HtmlEncoder.Default); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -59,18 +58,18 @@ public async Task CanRender_ParameterlessComponent_ClientMode() Assert.Equal("webassembly", marker.Type); Assert.Equal(typeof(TestComponent).Assembly.GetName().Name, marker.Assembly); Assert.Equal(typeof(TestComponent).FullName, marker.TypeName); - Assert.Empty(viewContext.Items); + Assert.Empty(httpContext.Items); } [Fact] public async Task CanPrerender_ParameterlessComponent_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.WebAssemblyPrerendered, ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -95,7 +94,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode() Assert.Null(epilogueMarker.Type); Assert.Null(epilogueMarker.ParameterDefinitions); Assert.Null(epilogueMarker.ParameterValues); - var (_, mode) = Assert.Single(viewContext.Items); + var (_, mode) = Assert.Single(httpContext.Items); var invoked = Assert.IsType(mode); Assert.Equal(InvokedRenderModes.Mode.WebAssembly, invoked.Value); } @@ -104,11 +103,11 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode() public async Task CanRender_ComponentWithParameters_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssembly, ParameterView.FromDictionary(new Dictionary { @@ -141,11 +140,11 @@ public async Task CanRender_ComponentWithParameters_ClientMode() public async Task CanRender_ComponentWithNullParameters_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssembly, ParameterView.FromDictionary(new Dictionary { @@ -176,11 +175,11 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode() public async Task CanPrerender_ComponentWithParameters_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, ParameterView.FromDictionary(new Dictionary { @@ -225,11 +224,11 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode() public async Task CanPrerender_ComponentWithNullParameters_ClientMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, ParameterView.FromDictionary(new Dictionary { @@ -273,11 +272,11 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode() public async Task CanRender_ParameterlessComponent() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.Static, ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); @@ -289,12 +288,12 @@ public async Task CanRender_ParameterlessComponent() public async Task CanRender_ParameterlessComponent_ServerMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -313,20 +312,20 @@ public async Task CanRender_ParameterlessComponent_ServerMode() Assert.Equal(typeof(TestComponent).FullName, serverComponent.TypeName); Assert.NotEqual(Guid.Empty, serverComponent.InvocationId); - Assert.Equal("no-cache, no-store, max-age=0", viewContext.HttpContext.Response.Headers.CacheControl); - Assert.DoesNotContain(viewContext.Items.Values, value => value is InvokedRenderModes); + Assert.Equal("no-cache, no-store, max-age=0", httpContext.Response.Headers.CacheControl); + Assert.DoesNotContain(httpContext.Items.Values, value => value is InvokedRenderModes); } [Fact] public async Task CanPrerender_ParameterlessComponent_ServerMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -357,8 +356,8 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode() Assert.Null(epilogueMarker.Descriptor); Assert.Null(epilogueMarker.Type); - Assert.Equal("no-cache, no-store, max-age=0", viewContext.HttpContext.Response.Headers.CacheControl); - var (_, mode) = Assert.Single(viewContext.Items, (kvp) => kvp.Value is InvokedRenderModes); + Assert.Equal("no-cache, no-store, max-age=0", httpContext.Response.Headers.CacheControl); + var (_, mode) = Assert.Single(httpContext.Items, (kvp) => kvp.Value is InvokedRenderModes); Assert.Equal(InvokedRenderModes.Mode.Server, ((InvokedRenderModes)mode).Value); } @@ -366,15 +365,15 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode() public async Task Prerender_ServerAndClientComponentUpdatesInvokedPrerenderModes() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var server = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); - var client = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, parameters); + var server = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var client = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, parameters); // Assert - var (_, mode) = Assert.Single(viewContext.Items, (kvp) => kvp.Value is InvokedRenderModes); + var (_, mode) = Assert.Single(httpContext.Items, (kvp) => kvp.Value is InvokedRenderModes); Assert.Equal(InvokedRenderModes.Mode.ServerAndWebAssembly, ((InvokedRenderModes)mode).Value); } @@ -382,16 +381,16 @@ public async Task Prerender_ServerAndClientComponentUpdatesInvokedPrerenderModes public async Task CanRenderMultipleServerComponents() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act - var firstResult = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var firstResult = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.ServerPrerendered, ParameterView.Empty); var firstComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(firstResult)); var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline); - var secondResult = await renderer.PrerenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); + var secondResult = await renderer.PrerenderComponentAsync(httpContext, typeof(TestComponent), RenderMode.Server, ParameterView.Empty); var secondComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(secondResult)); var secondMatch = Regex.Match(secondComponent, ComponentPattern); @@ -424,11 +423,11 @@ public async Task CanRenderMultipleServerComponents() public async Task CanRender_ComponentWithParametersObject() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Static, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); @@ -439,13 +438,13 @@ public async Task CanRender_ComponentWithParametersObject() public async Task CanRender_ComponentWithParameters_ServerMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -478,13 +477,13 @@ public async Task CanRender_ComponentWithParameters_ServerMode() public async Task CanRender_ComponentWithNullParameters_ServerMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default); var match = Regex.Match(content, ComponentPattern); @@ -517,13 +516,13 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode() public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -568,13 +567,13 @@ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) .ToTimeLimitedDataProtector(); // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -619,12 +618,12 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode public async Task ComponentWithInvalidRenderMode_Throws() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act & Assert var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); var ex = await ExceptionAssert.ThrowsArgumentAsync( - async () => await renderer.PrerenderComponentAsync(viewContext, typeof(GreetingComponent), default, parameters), + async () => await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), default, parameters), "prerenderMode", $"Unsupported RenderMode '{(RenderMode)default}'"); } @@ -633,12 +632,12 @@ public async Task ComponentWithInvalidRenderMode_Throws() public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act var state = new OnAfterRenderState(); var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(OnAfterRenderComponent), RenderMode.Static, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); @@ -663,13 +662,13 @@ public async Task DisposableComponents_GetDisposedAfterScopeCompletes() var scope = provider.GetRequiredService().CreateScope(); var scopedProvider = scope.ServiceProvider; var context = new DefaultHttpContext() { RequestServices = scopedProvider }; - var viewContext = GetViewContext(context); + var httpContext = GetHttpContext(context); var renderer = scopedProvider.GetRequiredService(); // Act var state = new AsyncDisposableState(); var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(AsyncDisposableComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncDisposableComponent), RenderMode.Static, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); @@ -683,11 +682,11 @@ public async Task DisposableComponents_GetDisposedAfterScopeCompletes() public async Task CanCatch_ComponentWithSynchronousException() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act & Assert var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( - viewContext, + httpContext, typeof(ExceptionComponent), RenderMode.Static, ParameterView.FromDictionary(new Dictionary @@ -703,11 +702,11 @@ public async Task CanCatch_ComponentWithSynchronousException() public async Task CanCatch_ComponentWithAsynchronousException() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act & Assert var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( - viewContext, + httpContext, typeof(ExceptionComponent), RenderMode.Static, ParameterView.FromDictionary(new Dictionary @@ -723,11 +722,11 @@ public async Task CanCatch_ComponentWithAsynchronousException() public async Task Rendering_ComponentWithJsInteropThrows() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); // Act & Assert var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( - viewContext, + httpContext, typeof(ExceptionComponent), RenderMode.Static, ParameterView.FromDictionary(new Dictionary @@ -755,11 +754,11 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse var responseMock = new Mock(); responseMock.Setup(r => r.HasStarted).Returns(true); ctx.Features.Set(responseMock.Object); - var viewContext = GetViewContext(ctx); + var httpContext = GetHttpContext(ctx); // Act var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( - viewContext, + httpContext, typeof(RedirectComponent), RenderMode.Static, ParameterView.FromDictionary(new Dictionary @@ -783,11 +782,11 @@ public async Task HtmlHelper_Redirects_WhenComponentNavigates() ctx.Request.PathBase = "/base"; ctx.Request.Path = "/path"; ctx.Request.QueryString = new QueryString("?query=value"); - var viewContext = GetViewContext(ctx); + var httpContext = GetHttpContext(ctx); // Act await renderer.PrerenderComponentAsync( - viewContext, + httpContext, typeof(RedirectComponent), RenderMode.Static, ParameterView.FromDictionary(new Dictionary @@ -804,7 +803,7 @@ await renderer.PrerenderComponentAsync( public async Task CanRender_AsyncComponent() { // Arrange - var viewContext = GetViewContext(); + var httpContext = GetHttpContext(); var expectedContent = @" @@ -849,7 +848,7 @@ public async Task CanRender_AsyncComponent()
"; // Act - var result = await renderer.PrerenderComponentAsync(viewContext, typeof(AsyncComponent), RenderMode.Static, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncComponent), RenderMode.Static, ParameterView.Empty); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentUtilities.HtmlContentToString(result, HtmlEncoder.Default)); // Assert @@ -863,7 +862,7 @@ private ComponentPrerenderer GetComponentRenderer(IServiceProvider services = nu new ServerComponentSerializer(_dataprotectorProvider)); } - private ViewContext GetViewContext(HttpContext context = null) + private HttpContext GetHttpContext(HttpContext context = null) { context ??= new DefaultHttpContext(); context.RequestServices ??= _services; @@ -873,7 +872,7 @@ private ViewContext GetViewContext(HttpContext context = null) context.Request.Path = "/path"; context.Request.QueryString = QueryString.FromUriComponent("?query=value"); - return new ViewContext { HttpContext = context }; + return context; } private static ServiceCollection CreateDefaultServiceCollection() diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/RazorComponentResultTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/RazorComponentResultTest.cs new file mode 100644 index 000000000000..9ebdbce6cfc2 --- /dev/null +++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/RazorComponentResultTest.cs @@ -0,0 +1,214 @@ +// 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; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Infrastructure; +using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Routing; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Mvc.ViewFeatures; + +public class RazorComponentResultTest +{ + [Fact] + public void AcceptsNullParameters() + { + var result = new RazorComponentResult(typeof(SimpleComponent), null); + Assert.NotNull(result.Parameters); + Assert.Empty(result.Parameters); + } + + [Fact] + public void AcceptsDictionaryParameters() + { + var paramsDict = new Dictionary { { "First", 123 } }; + var result = new RazorComponentResult(typeof(SimpleComponent), paramsDict); + Assert.Equal(1, result.Parameters.Count); + Assert.Equal(123, result.Parameters["First"]); + Assert.Same(paramsDict, result.Parameters); + } + + [Fact] + public void AcceptsObjectParameters() + { + var result = new RazorComponentResult(typeof(SimpleComponent), new { Param1 = 123, Param2 = "Another" }); + Assert.Equal(2, result.Parameters.Count); + Assert.Equal(123, result.Parameters["Param1"]); + Assert.Equal("Another", result.Parameters["Param2"]); + } + + [Fact] + public async Task CanRenderComponentStatically() + { + // Arrange + var result = new RazorComponentResult(); + var httpContext = GetTestHttpContext(); + var responseBody = new MemoryStream(); + httpContext.Response.Body = responseBody; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal("

Hello from SimpleComponent

", GetStringContent(responseBody)); + Assert.Equal("text/html; charset=utf-8", httpContext.Response.ContentType); + Assert.Equal(200, httpContext.Response.StatusCode); + } + + [Fact] + public async Task CanSetStatusCodeAndContentType() + { + // Arrange + var result = new RazorComponentResult + { + StatusCode = 123, + ContentType = "application/test-content-type", + }; + var httpContext = GetTestHttpContext(); + var responseBody = new MemoryStream(); + httpContext.Response.Body = responseBody; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal("

Hello from SimpleComponent

", GetStringContent(responseBody)); + Assert.Equal("application/test-content-type", httpContext.Response.ContentType); + Assert.Equal(123, httpContext.Response.StatusCode); + } + + [Fact] + public async Task WaitsForQuiescence() + { + // Arrange + var tcs = new TaskCompletionSource(); + var result = new RazorComponentResult(new { LoadingTask = tcs.Task }); + var httpContext = GetTestHttpContext(); + var responseBody = new MemoryStream(); + httpContext.Response.Body = responseBody; + + // Act/Assert: Doesn't complete until loading finishes + var completionTask = result.ExecuteAsync(httpContext); + await Task.Yield(); + Assert.False(completionTask.IsCompleted); + + // Act/Assert: Does complete when loading finishes + tcs.SetResult(); + await completionTask; + Assert.Equal("Loading task status: RanToCompletion", GetStringContent(responseBody)); + } + + [Fact] + public async Task SupportsLayouts() + { + // Arrange + var result = new RazorComponentResult(); + var httpContext = GetTestHttpContext(); + var responseBody = new MemoryStream(); + httpContext.Response.Body = responseBody; + + // Act + await result.ExecuteAsync(httpContext); + + // Assert + Assert.Equal("[TestParentLayout with content: [TestLayout with content: Page]]", GetStringContent(responseBody)); + } + + private static string GetStringContent(MemoryStream stream) + { + stream.Position = 0; + return new StreamReader(stream).ReadToEnd(); + } + + private static DefaultHttpContext GetTestHttpContext() + { + var serviceCollection = new ServiceCollection() + .AddSingleton(new DiagnosticListener("test")) + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddLogging(); + return new DefaultHttpContext { RequestServices = serviceCollection.BuildServiceProvider() }; + } + + class SimpleComponent : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "h1"); + builder.AddContent(1, "Hello from SimpleComponent"); + builder.CloseElement(); + } + } + + class AsyncLoadingComponent : ComponentBase + { + [Parameter] public Task LoadingTask { get; set; } + + protected override async Task OnInitializedAsync() + { + await LoadingTask; + await Task.Delay(100); // Doesn't strictly make any difference to the test, but clarifies that arbitrary async stuff could happen here + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + => builder.AddContent(0, $"Loading task status: {LoadingTask.Status}"); + } + + [Layout(typeof(TestLayout))] + class ComponentWithLayout : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + => builder.AddContent(0, $"Page"); + } + + [Layout(typeof(TestParentLayout))] + class TestLayout : LayoutComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"[{nameof(TestLayout)} with content: "); + builder.AddContent(1, Body); + builder.AddContent(2, "]"); + } + } + + class TestParentLayout : LayoutComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddContent(0, $"[{nameof(TestParentLayout)} with content: "); + builder.AddContent(1, Body); + builder.AddContent(2, "]"); + } + } + + class FakeDataProtectionProvider : IDataProtectionProvider + { + public IDataProtector CreateProtector(string purpose) + => new FakeDataProtector(); + + class FakeDataProtector : IDataProtector + { + public IDataProtector CreateProtector(string purpose) => throw new NotImplementedException(); + public byte[] Protect(byte[] plaintext) => throw new NotImplementedException(); + public byte[] Unprotect(byte[] protectedData) => throw new NotImplementedException(); + } + } + + class FakeNavigationManager : NavigationManager, IHostEnvironmentNavigationManager + { + public new void Initialize(string baseUri, string uri) { } + } +}