diff --git a/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml b/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml
index 7818ea3afd3a..4fe357cf81f1 100644
--- a/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml
+++ b/src/Components/Samples/BlazorServerApp/Pages/_Host.cshtml
@@ -13,9 +13,7 @@
-
- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered))
-
+
diff --git a/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs
new file mode 100644
index 000000000000..1aefbe2b8efd
--- /dev/null
+++ b/src/Components/test/E2ETest/ServerExecutionTests/ComponentWithParametersTest.cs
@@ -0,0 +1,60 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Linq;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
+using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
+using Microsoft.AspNetCore.E2ETesting;
+using OpenQA.Selenium;
+using TestServer;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
+{
+ public class ComponentWithParametersTest : ServerTestBase>
+ {
+ public ComponentWithParametersTest(
+ BrowserFixture browserFixture,
+ BasicTestAppServerSiteFixture serverFixture,
+ ITestOutputHelper output)
+ : base(browserFixture, serverFixture, output)
+ {
+ }
+
+ [Fact]
+ public void PassingParametersToComponentsFromThePageWorks()
+ {
+ Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");
+
+ BeginInteractivity();
+
+ Browser.Exists(By.CssSelector(".interactive"));
+
+ var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
+ Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
+ Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);
+
+ // The assigned value is of a more derived type than the declared model type. This check
+ // verifies we use the actual model type during round tripping.
+ var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
+ Assert.Equal("Value Derived-Value", parameter2.Text);
+
+ // This check verifies CaptureUnmatchedValues works
+ var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
+ Assert.Collection(
+ parameter3,
+ p => Assert.Equal("key1 testQueryValue", p.Text),
+ p => Assert.Equal("key2 43", p.Text));
+ }
+
+ private void BeginInteractivity()
+ {
+ Browser.FindElement(By.Id("load-boot-script")).Click();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor b/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor
new file mode 100644
index 000000000000..8ec03cf1f07e
--- /dev/null
+++ b/src/Components/test/testassets/BasicTestApp/ComponentWithParameters.razor
@@ -0,0 +1,48 @@
+Component With Parameters
+
+
+ @foreach (var value in Param1)
+ {
+ @value.StringProperty @value.IntProperty
+ }
+
+
+@* Making sure polymorphism works *@
+@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty
+
+@* Making sure CaptureUnmatchedValues works *@
+
+
+ @foreach (var value in Param3.OrderBy(kvp => kvp.Key))
+ {
+ @value.Key @value.Value
+ }
+
+
+@code
+{
+ [Parameter] public List Param1 { get; set; }
+
+ [Parameter] public TestModel Param2 { get; set; }
+
+ [Parameter(CaptureUnmatchedValues = true)] public IDictionary Param3 { get; set; }
+
+ private DerivedModel DerivedParam2 => (DerivedModel)Param2;
+
+ public static List TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();
+
+ public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };
+
+ public class TestModel
+ {
+
+ public string StringProperty { get; set; }
+
+ public int IntProperty { get; set; }
+ }
+
+ public class DerivedModel : TestModel
+ {
+ public string DerivedProperty { get; set; }
+ }
+}
diff --git a/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml b/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml
index 77d00063ca6f..9aa4ff85adcb 100644
--- a/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml
+++ b/src/Components/test/testassets/ComponentsApp.Server/Pages/_Host.cshtml
@@ -12,7 +12,7 @@
- @(await Html.RenderComponentAsync(RenderMode.Server))
+
+
+
+@functions
+{
+ [BindProperty(SupportsGet = true)]
+ public string QueryValue { get; set; }
+}
diff --git a/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml b/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml
index 61ce8d6efb35..c38770012977 100644
--- a/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml
+++ b/src/Components/test/testassets/TestServer/Pages/MultipleComponents.cshtml
@@ -12,24 +12,24 @@
@(await Html.RenderComponentAsync
(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync(RenderMode.Server))
- @(await Html.RenderComponentAsync(RenderMode.Static, new { Name = "John" }))
- @(await Html.RenderComponentAsync(RenderMode.Server))
+
+
Some content before
- @(await Html.RenderComponentAsync
(RenderMode.Server))
+
Some content between
- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered))
+
Some content after
- @(await Html.RenderComponentAsync(RenderMode.Server, new { Name = "Albert" }))
- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
+
+
diff --git a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml
index 1ba6f2f1c922..2d7eacfb9b9f 100644
--- a/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml
+++ b/src/Components/test/testassets/TestServer/Pages/PrerenderedHost.cshtml
@@ -1,5 +1,6 @@
@page
@using BasicTestApp.RouterTest
+
@@ -7,7 +8,7 @@
- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered))
+
@*
So that E2E tests can make assertions about both the prerendered and
diff --git a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml
index 668123d6f91f..af2f28f65854 100644
--- a/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml
+++ b/src/Components/test/testassets/TestServer/Pages/_ServerHost.cshtml
@@ -1,4 +1,5 @@
@page ""
+@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
@@ -11,7 +12,7 @@
- @(await Html.RenderComponentAsync(RenderMode.Server))
+
diff --git a/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml b/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml
new file mode 100644
index 000000000000..59d4a92b6d78
--- /dev/null
+++ b/src/Components/test/testassets/TestServer/Pages/_ViewImports.cshtml
@@ -0,0 +1,3 @@
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
+@using BasicTestApp
+
diff --git a/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs b/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs
index b940696196e8..ef7a54279007 100644
--- a/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs
+++ b/src/Mvc/Mvc.TagHelpers/ref/Microsoft.AspNetCore.Mvc.TagHelpers.netcoreapp.cs
@@ -105,6 +105,22 @@ public partial class CacheTagHelperOptions
public CacheTagHelperOptions() { }
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
}
+ [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
+ public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
+ {
+ public ComponentTagHelper() { }
+ [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
+ public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
+ public System.Collections.Generic.IDictionary Parameters { get { throw null; } set { } }
+ [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
+ public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ [Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
+ [Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
+ public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
+ [System.Diagnostics.DebuggerStepThroughAttribute]
+ public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
+ }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
{
diff --git a/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs
new file mode 100644
index 000000000000..6dc9bfe7224e
--- /dev/null
+++ b/src/Mvc/Mvc.TagHelpers/src/ComponentTagHelper.cs
@@ -0,0 +1,80 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Mvc.TagHelpers
+{
+ ///
+ /// A that renders a Razor component.
+ ///
+ [HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
+ public sealed class ComponentTagHelper : TagHelper
+ {
+ private const string ComponentParameterName = "params";
+ private const string ComponentParameterPrefix = "param-";
+ private const string ComponentTypeName = "type";
+ private const string RenderModeName = "render-mode";
+ private IDictionary _parameters;
+
+ ///
+ /// Gets or sets the for the current request.
+ ///
+ [HtmlAttributeNotBound]
+ [ViewContext]
+ public ViewContext ViewContext { get; set; }
+
+ ///
+ /// Gets or sets values for component parameters.
+ ///
+ [HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
+ public IDictionary Parameters
+ {
+ get
+ {
+ _parameters ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+ return _parameters;
+ }
+ set => _parameters = value;
+ }
+
+ ///
+ /// Gets or sets the component type. This value is required.
+ ///
+ [HtmlAttributeName(ComponentTypeName)]
+ public Type ComponentType { get; set; }
+
+ ///
+ /// Gets or sets the
+ ///
+ [HtmlAttributeName(RenderModeName)]
+ public RenderMode RenderMode { get; set; }
+
+ ///
+ public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ if (output == null)
+ {
+ throw new ArgumentNullException(nameof(output));
+ }
+
+ var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService();
+ var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);
+
+ // Reset the TagName. We don't want `component` to render.
+ output.TagName = null;
+ output.Content.SetHtmlContent(result);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs
new file mode 100644
index 000000000000..5c1aff4a7eea
--- /dev/null
+++ b/src/Mvc/Mvc.TagHelpers/test/ComponentTagHelperTest.cs
@@ -0,0 +1,75 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO.Pipes;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.TagHelpers
+{
+ public class ComponentTagHelperTest
+ {
+ [Fact]
+ public async Task ProcessAsync_RendersComponent()
+ {
+ // Arrange
+ var tagHelper = new ComponentTagHelper
+ {
+ ViewContext = GetViewContext(),
+ };
+ var context = GetTagHelperContext();
+ var output = GetTagHelperOutput();
+
+ // Act
+ await tagHelper.ProcessAsync(context, output);
+
+ // Assert
+ var content = HtmlContentUtilities.HtmlContentToString(output.Content);
+ Assert.Equal("Hello world", content);
+ Assert.Null(output.TagName);
+ }
+
+ private static TagHelperContext GetTagHelperContext()
+ {
+ return new TagHelperContext(
+ "component",
+ new TagHelperAttributeList(),
+ new Dictionary(),
+ Guid.NewGuid().ToString("N"));
+ }
+
+ private static TagHelperOutput GetTagHelperOutput()
+ {
+ return new TagHelperOutput(
+ "component",
+ new TagHelperAttributeList(),
+ (_, __) => Task.FromResult(new DefaultTagHelperContent()));
+ }
+
+ private ViewContext GetViewContext()
+ {
+ var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
+ var renderer = Mock.Of(c =>
+ c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent));
+
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = new ServiceCollection().AddSingleton(renderer).BuildServiceProvider(),
+ };
+
+ return new ViewContext
+ {
+ HttpContext = httpContext,
+ };
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs
index e9618a4842d4..dfbd0dec1b04 100644
--- a/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs
+++ b/src/Mvc/Mvc.ViewFeatures/ref/Microsoft.AspNetCore.Mvc.ViewFeatures.netcoreapp.cs
@@ -324,8 +324,8 @@ public enum Html5DateRenderingMode
}
public static partial class HtmlHelperComponentExtensions
{
+ public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; }
public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
- [System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
}
public static partial class HtmlHelperDisplayExtensions
diff --git a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
index 7b17edbe29eb..84ffe50ce1c2 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs
@@ -18,7 +18,6 @@
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
-using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Microsoft.JSInterop;
@@ -206,8 +205,9 @@ internal static void AddViewServices(IServiceCollection services)
services.TryAddSingleton();
//
- // Component prerendering
+ // Component rendering
//
+ services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
services.TryAddScoped();
diff --git a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs
deleted file mode 100644
index 303c820cc92f..000000000000
--- a/src/Mvc/Mvc.ViewFeatures/src/HtmlHelperComponentExtensions.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Html;
-using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Mvc.ViewFeatures;
-using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Microsoft.AspNetCore.Mvc.Rendering
-{
- ///
- /// Extensions for rendering components.
- ///
- public static class HtmlHelperComponentExtensions
- {
- private static readonly object ComponentSequenceKey = new object();
-
- ///
- /// Renders the .
- ///
- /// The .
- /// The for the component.
- /// The HTML produced by the rendered .
- public static Task RenderComponentAsync(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
- {
- if (htmlHelper == null)
- {
- throw new ArgumentNullException(nameof(htmlHelper));
- }
-
- return htmlHelper.RenderComponentAsync(renderMode, null);
- }
-
- ///
- /// Renders the .
- ///
- /// The .
- /// An containing the parameters to pass
- /// to the component.
- /// The for the component.
- /// The HTML produced by the rendered .
- public static async Task RenderComponentAsync(
- this IHtmlHelper htmlHelper,
- RenderMode renderMode,
- object parameters) where TComponent : IComponent
- {
- if (htmlHelper == null)
- {
- throw new ArgumentNullException(nameof(htmlHelper));
- }
-
- var context = htmlHelper.ViewContext.HttpContext;
- return renderMode switch
- {
- RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
- RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
- RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)),
- _ => throw new ArgumentException("Invalid render mode", nameof(renderMode)),
- };
- }
-
- private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
- {
- if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
- {
- result = new ServerComponentInvocationSequence();
- viewContext.Items[ComponentSequenceKey] = result;
- }
-
- return (ServerComponentInvocationSequence)result;
- }
-
- private static ParameterView GetParametersCollection(object parameters) => parameters == null ?
- ParameterView.Empty :
- ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
-
- private static async Task StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
- {
- var serviceProvider = context.RequestServices;
- var prerenderer = serviceProvider.GetRequiredService();
-
-
- var result = await prerenderer.PrerenderComponentAsync(
- parametersCollection,
- context,
- type);
-
- return new ComponentHtmlContent(result);
- }
-
- private static async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
- {
- var serviceProvider = context.RequestServices;
- var prerenderer = serviceProvider.GetRequiredService();
- var invocationSerializer = serviceProvider.GetRequiredService();
-
- var currentInvocation = invocationSerializer.SerializeInvocation(
- invocationId,
- type,
- parametersCollection,
- prerendered: true);
-
- var result = await prerenderer.PrerenderComponentAsync(
- parametersCollection,
- context,
- type);
-
- return new ComponentHtmlContent(
- invocationSerializer.GetPreamble(currentInvocation),
- result,
- invocationSerializer.GetEpilogue(currentInvocation));
- }
-
- private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
- {
- var serviceProvider = context.RequestServices;
- var invocationSerializer = serviceProvider.GetRequiredService();
- var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
-
- return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
- }
- }
-}
diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs
new file mode 100644
index 000000000000..44a618fb04a1
--- /dev/null
+++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/ComponentRenderer.cs
@@ -0,0 +1,110 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+ internal class ComponentRenderer : IComponentRenderer
+ {
+ private static readonly object ComponentSequenceKey = new object();
+ private readonly StaticComponentRenderer _staticComponentRenderer;
+ private readonly ServerComponentSerializer _serverComponentSerializer;
+
+ public ComponentRenderer(
+ StaticComponentRenderer staticComponentRenderer,
+ ServerComponentSerializer serverComponentSerializer)
+ {
+ _staticComponentRenderer = staticComponentRenderer;
+ _serverComponentSerializer = serverComponentSerializer;
+ }
+
+ public async Task RenderComponentAsync(
+ ViewContext viewContext,
+ Type componentType,
+ RenderMode renderMode,
+ object parameters)
+ {
+ if (viewContext is null)
+ {
+ throw new ArgumentNullException(nameof(viewContext));
+ }
+
+ if (componentType is null)
+ {
+ throw new ArgumentNullException(nameof(componentType));
+ }
+
+ if (!typeof(IComponent).IsAssignableFrom(componentType))
+ {
+ throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent)));
+ }
+
+ var context = viewContext.HttpContext;
+ var parameterView = parameters is null ?
+ ParameterView.Empty :
+ ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
+
+ return renderMode switch
+ {
+ RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
+ RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
+ RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
+ _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
+ };
+ }
+
+ private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
+ {
+ if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
+ {
+ result = new ServerComponentInvocationSequence();
+ viewContext.Items[ComponentSequenceKey] = result;
+ }
+
+ return (ServerComponentInvocationSequence)result;
+ }
+
+ private async Task StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
+ {
+ var result = await _staticComponentRenderer.PrerenderComponentAsync(
+ parametersCollection,
+ context,
+ type);
+
+ return new ComponentHtmlContent(result);
+ }
+
+ private async Task PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
+ {
+ var currentInvocation = _serverComponentSerializer.SerializeInvocation(
+ invocationId,
+ type,
+ parametersCollection,
+ prerendered: true);
+
+ var result = await _staticComponentRenderer.PrerenderComponentAsync(
+ parametersCollection,
+ context,
+ type);
+
+ return new ComponentHtmlContent(
+ _serverComponentSerializer.GetPreamble(currentInvocation),
+ result,
+ _serverComponentSerializer.GetEpilogue(currentInvocation));
+ }
+
+ private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
+ {
+ var serviceProvider = context.RequestServices;
+ var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
+
+ return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs
new file mode 100644
index 000000000000..90df64f9f764
--- /dev/null
+++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/IComponentRenderer.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
+{
+ internal interface IComponentRenderer
+ {
+ Task RenderComponentAsync(
+ ViewContext viewContext,
+ Type componentType,
+ RenderMode renderMode,
+ object parameters);
+ }
+}
diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
index 72e89faf5fe2..89304873c108 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/StaticComponentRenderer.cs
@@ -11,11 +11,10 @@
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions;
-using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
-namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class StaticComponentRenderer
{
diff --git a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs
index bcb36c37a030..da8402029475 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs
+++ b/src/Mvc/Mvc.ViewFeatures/src/RazorComponents/UnsupportedJavaScriptRuntime.cs
@@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.JSInterop;
diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs
new file mode 100644
index 000000000000..16f021574aff
--- /dev/null
+++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs
@@ -0,0 +1,70 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Mvc.Rendering
+{
+ ///
+ /// Extensions for rendering components.
+ ///
+ public static class HtmlHelperComponentExtensions
+ {
+ ///
+ /// Renders the .
+ ///
+ /// The .
+ /// The for the component.
+ /// The HTML produced by the rendered .
+ public static Task RenderComponentAsync(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
+ => RenderComponentAsync(htmlHelper, renderMode, parameters: null);
+
+ ///
+ /// Renders the .
+ ///
+ /// The .
+ /// An containing the parameters to pass
+ /// to the component.
+ /// The for the component.
+ /// The HTML produced by the rendered .
+ public static Task RenderComponentAsync(
+ this IHtmlHelper htmlHelper,
+ RenderMode renderMode,
+ object parameters) where TComponent : IComponent
+ => RenderComponentAsync(htmlHelper, typeof(TComponent), renderMode, parameters);
+
+ ///
+ /// Renders the specified .
+ ///
+ /// The .
+ /// The component type.
+ /// An containing the parameters to pass
+ /// to the component.
+ /// The for the component.
+ public static Task RenderComponentAsync(
+ this IHtmlHelper htmlHelper,
+ Type componentType,
+ RenderMode renderMode,
+ object parameters)
+ {
+ if (htmlHelper is null)
+ {
+ throw new ArgumentNullException(nameof(htmlHelper));
+ }
+
+ if (componentType is null)
+ {
+ throw new ArgumentNullException(nameof(componentType));
+ }
+
+ var viewContext = htmlHelper.ViewContext;
+ var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService();
+ return componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters);
+ }
+ }
+}
diff --git a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx
index e7362a9bf5ac..ae6a29aa8173 100644
--- a/src/Mvc/Mvc.ViewFeatures/src/Resources.resx
+++ b/src/Mvc/Mvc.ViewFeatures/src/Resources.resx
@@ -295,4 +295,7 @@
Unsupported data type '{0}'.
+
+ Unsupported RenderMode '{0}'.
+
\ No newline at end of file
diff --git a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
similarity index 83%
rename from src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs
rename to src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
index a7629497a724..1e89472473d8 100644
--- a/src/Mvc/Mvc.ViewFeatures/test/HtmlHelperComponentExtensionsTests.cs
+++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/ComponentRendererTest.cs
@@ -13,7 +13,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering;
-using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
+using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -22,24 +22,26 @@
using Moq;
using Xunit;
-namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
+namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
- public class HtmlHelperComponentExtensionsTests
+ public class ComponentRendererTest
{
private const string PrerenderedServerComponentPattern = "^(?.+?)$";
private const string ServerComponentPattern = "^$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
+ private readonly ComponentRenderer renderer = GetComponentRenderer();
+
[Fact]
public async Task CanRender_ParameterlessComponent()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
var writer = new StringWriter();
// Act
- var result = await helper.RenderComponentAsync(RenderMode.Static);
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
@@ -51,15 +53,13 @@ public async Task CanRender_ParameterlessComponent()
public async Task CanRender_ParameterlessComponent_ServerMode()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(RenderMode.Server);
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
// Assert
@@ -82,15 +82,13 @@ public async Task CanRender_ParameterlessComponent_ServerMode()
public async Task CanPrerender_ParameterlessComponent_ServerMode()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(RenderMode.ServerPrerendered);
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
@@ -125,21 +123,17 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode()
public async Task CanRenderMultipleServerComponents()
{
// Arrange
- var helper = CreateHelper();
- var firstWriter = new StringWriter();
- var secondWriter = new StringWriter();
+ var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var firstResult = await helper.RenderComponentAsync(RenderMode.ServerPrerendered);
- firstResult.WriteTo(firstWriter, HtmlEncoder.Default);
- var firstComponent = firstWriter.ToString();
+ var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
+ var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
- var secondResult = await helper.RenderComponentAsync(RenderMode.Server);
- secondResult.WriteTo(secondWriter, HtmlEncoder.Default);
- var secondComponent = secondWriter.ToString();
+ var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
+ var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
// Assert
@@ -171,20 +165,13 @@ public async Task CanRenderMultipleServerComponents()
public async Task CanRender_ComponentWithParametersObject()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
// Act
- var result = await helper.RenderComponentAsync(
- RenderMode.Static,
- new
- {
- Name = "Steve"
- });
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" });
// Assert
+ var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("Hello Steve!
", content);
}
@@ -192,20 +179,13 @@ public async Task CanRender_ComponentWithParametersObject()
public async Task CanRender_ComponentWithParameters_ServerMode()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(
- RenderMode.Server,
- new
- {
- Name = "Daniel"
- });
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
// Assert
@@ -237,20 +217,14 @@ public async Task CanRender_ComponentWithParameters_ServerMode()
public async Task CanRender_ComponentWithNullParameters_ServerMode()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(
- RenderMode.Server,
- new
- {
- Name = (string)null
- });
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, ServerComponentPattern);
// Assert
@@ -274,28 +248,22 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode()
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
- var value = Assert.Single(serverComponent.ParameterValues);;
+ var value = Assert.Single(serverComponent.ParameterValues); ;
Assert.Null(value);
}
[Fact]
- public async Task CanPrerender_ComponentWithParameters_ServerMode()
+ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(
- RenderMode.ServerPrerendered,
- new
- {
- Name = "Daniel"
- });
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
@@ -336,23 +304,17 @@ public async Task CanPrerender_ComponentWithParameters_ServerMode()
}
[Fact]
- public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
+ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
- var result = await helper.RenderComponentAsync(
- RenderMode.ServerPrerendered,
- new
- {
- Name = (string)null
- });
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
+ var content = HtmlContentUtilities.HtmlContentToString(result);
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
@@ -396,39 +358,28 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
public async Task ComponentWithInvalidRenderMode_Throws()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
// Act & Assert
- var result = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(
- default,
- new
- {
- Name = "Steve"
- }));
- Assert.Equal("renderMode", result.ParamName);
+ var ex = await ExceptionAssert.ThrowsArgumentAsync(
+ () => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }),
+ "renderMode",
+ $"Unsupported RenderMode '{(RenderMode)default}'");
}
[Fact]
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
// Act
var state = new OnAfterRenderState();
- var result = await helper.RenderComponentAsync(
- RenderMode.Static,
- new
- {
- State = state
- });
-
- result.WriteTo(writer, HtmlEncoder.Default);
+ var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state });
// Assert
- Assert.Equal("Hello
", writer.ToString());
+ var content = HtmlContentUtilities.HtmlContentToString(result);
+ Assert.Equal("Hello
", content);
Assert.False(state.OnAfterRenderRan);
}
@@ -436,10 +387,12 @@ public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
public async Task CanCatch_ComponentWithSynchronousException()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
// Act & Assert
- var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(
+ var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync(
+ viewContext,
+ typeof(ExceptionComponent),
RenderMode.Static,
new
{
@@ -454,10 +407,12 @@ public async Task CanCatch_ComponentWithSynchronousException()
public async Task CanCatch_ComponentWithAsynchronousException()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
// Act & Assert
- var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(
+ var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync(
+ viewContext,
+ typeof(ExceptionComponent),
RenderMode.Static,
new
{
@@ -472,10 +427,12 @@ public async Task CanCatch_ComponentWithAsynchronousException()
public async Task Rendering_ComponentWithJsInteropThrows()
{
// Arrange
- var helper = CreateHelper();
+ var viewContext = GetViewContext();
// Act & Assert
- var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(
+ var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync(
+ viewContext,
+ typeof(ExceptionComponent),
RenderMode.Static,
new
{
@@ -503,11 +460,12 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse
var responseMock = new Mock();
responseMock.Setup(r => r.HasStarted).Returns(true);
ctx.Features.Set(responseMock.Object);
- var helper = CreateHelper(ctx);
- var writer = new StringWriter();
+ var viewContext = GetViewContext(ctx);
// Act
- var exception = await Assert.ThrowsAsync(() => helper.RenderComponentAsync(
+ var exception = await Assert.ThrowsAsync(() => renderer.RenderComponentAsync(
+ viewContext,
+ typeof(RedirectComponent),
RenderMode.Static,
new
{
@@ -515,8 +473,8 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse
}));
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
- "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
- "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
+ "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
+ "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
exception.Message);
}
@@ -530,10 +488,12 @@ public async Task HtmlHelper_Redirects_WhenComponentNavigates()
ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value");
- var helper = CreateHelper(ctx);
+ var viewContext = GetViewContext(ctx);
// Act
- await helper.RenderComponentAsync(
+ await renderer.RenderComponentAsync(
+ viewContext,
+ typeof(RedirectComponent),
RenderMode.Static,
new
{
@@ -549,8 +509,7 @@ await helper.RenderComponentAsync(
public async Task CanRender_AsyncComponent()
{
// Arrange
- var helper = CreateHelper();
- var writer = new StringWriter();
+ var viewContext = GetViewContext();
var expectedContent = @"
@@ -595,29 +554,29 @@ public async Task CanRender_AsyncComponent()
";
// Act
- var result = await helper.RenderComponentAsync(RenderMode.Static);
- result.WriteTo(writer, HtmlEncoder.Default);
- var content = writer.ToString();
+ var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
+ var content = HtmlContentUtilities.HtmlContentToString(result);
// Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
}
- private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action configureServices = null)
+ private static ComponentRenderer GetComponentRenderer() =>
+ new ComponentRenderer(
+ new StaticComponentRenderer(HtmlEncoder.Default),
+ new ServerComponentSerializer(_dataprotectorProvider));
+
+ private static ViewContext GetViewContext(HttpContext context = null, Action configureServices = null)
{
var services = new ServiceCollection();
- services.AddSingleton(HtmlEncoder.Default);
- services.AddSingleton();
services.AddSingleton(_dataprotectorProvider);
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
services.AddSingleton();
configureServices?.Invoke(services);
- var helper = new Mock();
- var context = ctx ?? new DefaultHttpContext();
+ context ??= new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost");
@@ -625,12 +584,7 @@ private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action h.ViewContext)
- .Returns(new ViewContext()
- {
- HttpContext = context
- });
- return helper.Object;
+ return new ViewContext { HttpContext = context };
}
private class TestComponent : IComponent
diff --git a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs
index 7a670696fbe6..67778e416355 100644
--- a/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs
+++ b/src/Mvc/Mvc.ViewFeatures/test/RazorComponents/HtmlRendererTest.cs
@@ -6,13 +6,11 @@
using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
-namespace Microsoft.AspNetCore.Mvc.RazorComponents
+namespace Microsoft.AspNetCore.Components.Rendering
{
public class HtmlRendererTest
{
diff --git a/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs
new file mode 100644
index 000000000000..470b4b3ab04b
--- /dev/null
+++ b/src/Mvc/Mvc.ViewFeatures/test/Rendering/HtmlHelperComponentExtensionsTest.cs
@@ -0,0 +1,56 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Html;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.Extensions.DependencyInjection;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Mvc.Rendering
+{
+ public class HtmlHelperComponentExtensionsTest
+ {
+ [Fact]
+ public async Task RenderComponentAsync_Works()
+ {
+ // Arrange
+ var viewContext = GetViewContext();
+ var htmlHelper = Mock.Of(h => h.ViewContext == viewContext);
+
+ // Act
+ var result = await HtmlHelperComponentExtensions.RenderComponentAsync(htmlHelper, RenderMode.Static);
+
+ // Assert
+ Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(result));
+ }
+
+ private static ViewContext GetViewContext()
+ {
+ var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
+ var renderer = Mock.Of(c =>
+ c.RenderComponentAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()) == Task.FromResult(htmlContent));
+
+ var httpContext = new DefaultHttpContext
+ {
+ RequestServices = new ServiceCollection().AddSingleton(renderer).BuildServiceProvider(),
+ };
+
+ var viewContext = new ViewContext { HttpContext = httpContext };
+ return viewContext;
+ }
+
+ private class TestComponent : IComponent
+ {
+ public void Attach(RenderHandle renderHandle)
+ {
+ }
+
+ public Task SetParametersAsync(ParameterView parameters) => null;
+ }
+ }
+}
diff --git a/src/Mvc/samples/MvcSandbox/Components/App.razor b/src/Mvc/samples/MvcSandbox/Components/App.razor
index 2bf3672b80d1..1c9b07ba2087 100644
--- a/src/Mvc/samples/MvcSandbox/Components/App.razor
+++ b/src/Mvc/samples/MvcSandbox/Components/App.razor
@@ -1,2 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing
-
\ No newline at end of file
+@using MvcSandbox.Components.Shared
+
+
+
+
+
+
+
+ Sorry, there's nothing at this address.
+
+
+
diff --git a/src/Mvc/samples/MvcSandbox/Components/NotFound.razor b/src/Mvc/samples/MvcSandbox/Components/NotFound.razor
deleted file mode 100644
index 369bfb8dde19..000000000000
--- a/src/Mvc/samples/MvcSandbox/Components/NotFound.razor
+++ /dev/null
@@ -1,4 +0,0 @@
-@using MvcSandbox.Components.Shared
-@layout MainLayout
-Not Found
-Sorry, nothing was found.
\ No newline at end of file
diff --git a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml
index cdafd22f6864..89401bf85b33 100644
--- a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml
+++ b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml
@@ -1,5 +1,4 @@
@page
-@model MvcSandbox.Pages.ComponentsModel
@{
Layout = null;
}
@@ -15,8 +14,7 @@
- @(await Html.RenderComponentAsync(RenderMode.Static))
-
-
+
+
diff --git a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs b/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs
deleted file mode 100644
index fce8214cb5f3..000000000000
--- a/src/Mvc/samples/MvcSandbox/Pages/Components.cshtml.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.RazorPages;
-
-namespace MvcSandbox.Pages
-{
- public class ComponentsModel : PageModel
- {
- public void OnGet()
- {
- }
- }
-}
\ No newline at end of file
diff --git a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml
index a3da90da304a..1b670c2b6540 100644
--- a/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml
+++ b/src/Mvc/samples/MvcSandbox/Views/Shared/_Layout.cshtml
@@ -19,6 +19,9 @@
Pages Home
+
+ Components
+
diff --git a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml
index 7a104af51d00..14d8ae4c3eaf 100644
--- a/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml
+++ b/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorServerWeb-CSharp/Pages/_Host.cshtml
@@ -14,7 +14,7 @@
- @(await Html.RenderComponentAsync(RenderMode.ServerPrerendered))
+