Support parameters on server-side rendered components

Fixes https://github.com/aspnet/AspNetCore/issues/14433
This commit is contained in:
Javier Calvarro Nelson 2019-09-26 02:22:29 -07:00 committed by Artak
parent cc368c8e08
commit 6bc4d27bfa
19 changed files with 675 additions and 43 deletions

View File

@ -111,8 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var count = Descriptors.Count;
for (var i = 0; i < count; i++)
{
var (componentType, sequence) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, sequence.ToString());
var (componentType, parameters, sequence) = Descriptors[i];
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
}
Log.InitializationSucceeded(_logger);

View File

@ -9,12 +9,11 @@ namespace Microsoft.AspNetCore.Components.Server
{
public Type ComponentType { get; set; }
public ParameterView Parameters { get; set; }
public int Sequence { get; set; }
public void Deconstruct(out Type componentType, out int sequence)
{
componentType = ComponentType;
sequence = Sequence;
}
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
}
}

View File

@ -0,0 +1,188 @@
// 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.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Components.Server
{
internal class ComponentParameterDeserializer
{
private readonly ILogger<ComponentParameterDeserializer> _logger;
private readonly ComponentParametersTypeCache _parametersCache;
public ComponentParameterDeserializer(
ILogger<ComponentParameterDeserializer> logger,
ComponentParametersTypeCache parametersCache)
{
_logger = logger;
_parametersCache = parametersCache;
}
public bool TryDeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues, out ParameterView parameters)
{
parameters = default;
var parametersDictionary = new Dictionary<string, object>();
if (parameterValues.Count != parametersDefinitions.Count)
{
// Mismatched number of definition/parameter values.
Log.MismatchedParameterAndDefinitions(_logger, parametersDefinitions.Count, parameterValues.Count);
return false;
}
for (var i = 0; i < parametersDefinitions.Count; i++)
{
var definition = parametersDefinitions[i];
if (definition.Name == null)
{
Log.MissingParameterDefinitionName(_logger);
return false;
}
if (definition.TypeName == null && definition.Assembly == null)
{
parametersDictionary.Add(definition.Name, null);
}
else if (definition.TypeName == null || definition.Assembly == null)
{
Log.IncompleteParameterDefinition(_logger, definition.Name, definition.TypeName, definition.Assembly);
return false;
}
else
{
var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
if (parameterType == null)
{
Log.InvalidParameterType(_logger, definition.Name, definition.Assembly, definition.TypeName);
return false;
}
try
{
// At this point we know the parameter is not null, as we don't serialize the type name or the assembly name
// for null parameters.
var value = (JsonElement)parameterValues[i];
var parameterValue = JsonSerializer.Deserialize(
value.GetRawText(),
parameterType,
ServerComponentSerializationSettings.JsonSerializationOptions);
parametersDictionary.Add(definition.Name, parameterValue);
}
catch (Exception e)
{
Log.InvalidParameterValue(_logger, definition.Name, definition.TypeName, definition.Assembly, e);
return false;
}
}
}
parameters = ParameterView.FromDictionary(parametersDictionary);
return true;
}
private ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
{
try
{
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, ServerComponentSerializationSettings.JsonSerializationOptions);
}
catch (Exception e)
{
Log.FailedToParseParameterDefinitions(_logger, e);
return null;
}
}
private JsonDocument GetParameterValues(string parameterValues)
{
try
{
return JsonDocument.Parse(parameterValues);
}
catch (Exception e)
{
Log.FailedToParseParameterValues(_logger, e);
return null;
}
}
private static class Log
{
private static readonly Action<ILogger, Exception> _parameterValuesInvalidFormat =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(1, "ParameterValuesInvalidFormat"),
"Parameter values must be an array.");
private static readonly Action<ILogger, string, string, string, Exception> _incompleteParameterDefinition =
LoggerMessage.Define<string, string, string>(
LogLevel.Debug,
new EventId(2, "IncompleteParameterDefinition"),
"The parameter definition for '{ParameterName}' is incomplete: Type='{TypeName}' Assembly='{Assembly}'.");
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterType =
LoggerMessage.Define<string, string, string>(
LogLevel.Debug,
new EventId(3, "InvalidParameterType"),
"The parameter '{ParameterName} with type '{TypeName}' in assembly '{Assembly}' could not be found.");
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterValue =
LoggerMessage.Define<string, string, string>(
LogLevel.Debug,
new EventId(4, "InvalidParameterValue"),
"Could not parse the parameter value for parameter '{Name}' of type '{TypeName}' and assembly '{Assembly}'.");
private static readonly Action<ILogger, Exception> _failedToParseParameterDefinitions =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(5, "FailedToParseParameterDefinitions"),
"Failed to parse the parameter definitions.");
private static readonly Action<ILogger, Exception> _failedToParseParameterValues =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(6, "FailedToParseParameterValues"),
"Failed to parse the parameter values.");
private static readonly Action<ILogger, int, int, Exception> _mismatchedParameterAndDefinitions =
LoggerMessage.Define<int, int>(
LogLevel.Debug,
new EventId(7, "MismatchedParameterAndDefinitions"),
"The number of parameter definitions '{DescriptorsLength}' does not match the number parameter values '{ValuesLength}'.");
private static readonly Action<ILogger, Exception> _missingParameterDefinitionName =
LoggerMessage.Define(
LogLevel.Debug,
new EventId(8, "MissingParameterDefinitionName"),
"The name is missing in a parameter definition.");
internal static void ParameterValuesInvalidFormat(ILogger<ComponentParameterDeserializer> logger) =>
_parameterValuesInvalidFormat(logger, null);
internal static void IncompleteParameterDefinition(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly) =>
_incompleteParameterDefinition(logger, name, typeName, assembly, null);
internal static void InvalidParameterType(ILogger<ComponentParameterDeserializer> logger, string name, string assembly, string typeName) =>
_invalidParameterType(logger, name, assembly, typeName, null);
internal static void InvalidParameterValue(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly, Exception e) =>
_invalidParameterValue(logger, name, typeName, assembly,e);
internal static void FailedToParseParameterDefinitions(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
_failedToParseParameterDefinitions(logger, e);
internal static void FailedToParseParameterValues(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
_failedToParseParameterValues(logger, e);
internal static void MismatchedParameterAndDefinitions(ILogger<ComponentParameterDeserializer> logger, int definitionsLength, int valuesLength) =>
_mismatchedParameterAndDefinitions(logger, definitionsLength, valuesLength, null);
internal static void MissingParameterDefinitionName(ILogger<ComponentParameterDeserializer> logger) =>
_missingParameterDefinitionName(logger, null);
}
}
}

View File

@ -0,0 +1,58 @@
// 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.Concurrent;
using System.Linq;
using System.Reflection;
namespace Microsoft.AspNetCore.Components
{
internal class ComponentParametersTypeCache
{
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
public Type GetParameterType(string assembly, string type)
{
var key = new Key(assembly, type);
if (_typeToKeyLookUp.TryGetValue(key, out var resolvedType))
{
return resolvedType;
}
else
{
return _typeToKeyLookUp.GetOrAdd(key, ResolveType, AppDomain.CurrentDomain.GetAssemblies());
}
}
private static Type ResolveType(Key key, Assembly[] assemblies)
{
var assembly = assemblies
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
if (assembly == null)
{
return null;
}
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
}
private struct Key : IEquatable<Key>
{
public Key(string assembly, string type) =>
(Assembly, Type) = (assembly, type);
public string Assembly { get; set; }
public string Type { get; set; }
public override bool Equals(object obj) => Equals((Key)obj);
public bool Equals(Key other) => string.Equals(Assembly, other.Assembly, StringComparison.Ordinal) &&
string.Equals(Type, other.Type, StringComparison.Ordinal);
public override int GetHashCode() => HashCode.Combine(Assembly, Type);
}
}
}

View File

@ -64,6 +64,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return RenderRootComponentAsync(componentId);
}
/// <summary>
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
/// causing it to be displayed in the specified DOM element.
/// </summary>
/// <param name="componentType">The type of the component.</param>
/// <param name="parameters">The parameters for the component.</param>
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
{
var component = InstantiateComponent(componentType);
var componentId = AssignRootComponentId(component);
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
CaptureAsyncExceptions(attachComponentTask);
return RenderRootComponentAsync(componentId, parameters);
}
protected override void ProcessPendingRender()
{
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)

View File

@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Server
// 'sequence' indicates the order in which this component got rendered on the server.
// 'assemblyName' the assembly name for the rendered component.
// 'type' the full type name for the rendered component.
// 'parameterDefinitions' a JSON serialized array that contains the definitions for the parameters including their names and types and assemblies.
// 'parameterValues' a JSON serialized array containing the parameter values.
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
// With parameters
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>", parameterDefinitions: "[{ \"name\":\"Parameter\", \"typeName\":\"string\", \"assembly\":\"System.Private.CoreLib\"}], parameterValues: [<<string-value>>]}))
// Serialization:
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
@ -55,11 +59,13 @@ namespace Microsoft.AspNetCore.Components.Server
private readonly IDataProtector _dataProtector;
private readonly ILogger<ServerComponentDeserializer> _logger;
private readonly ServerComponentTypeCache _rootComponentTypeCache;
private readonly ComponentParameterDeserializer _parametersDeserializer;
public ServerComponentDeserializer(
IDataProtectionProvider dataProtectionProvider,
ILogger<ServerComponentDeserializer> logger,
ServerComponentTypeCache rootComponentTypeCache)
ServerComponentTypeCache rootComponentTypeCache,
ComponentParameterDeserializer parametersDeserializer)
{
// When we protect the data we use a time-limited data protector with the
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
@ -74,6 +80,7 @@ namespace Microsoft.AspNetCore.Components.Server
_logger = logger;
_rootComponentTypeCache = rootComponentTypeCache;
_parametersDeserializer = parametersDeserializer;
}
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
@ -176,9 +183,16 @@ namespace Microsoft.AspNetCore.Components.Server
return default;
}
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
{
// TryDeserializeParameters does appropriate logging.
return default;
}
var componentDescriptor = new ComponentDescriptor
{
ComponentType = componentType,
Parameters = parameters,
Sequence = serverComponent.Sequence
};

View File

@ -58,6 +58,8 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<CircuitFactory>();
services.TryAddSingleton<ServerComponentDeserializer>();
services.TryAddSingleton<ServerComponentTypeCache>();
services.TryAddSingleton<ComponentParameterDeserializer>();
services.TryAddSingleton<ComponentParametersTypeCache>();
services.TryAddSingleton<CircuitIdFactory>();
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);

View File

@ -70,6 +70,7 @@
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>

View File

@ -2,6 +2,7 @@
// 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.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@ -40,6 +41,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.Equal(0, deserializedDescriptor.Sequence);
}
[Fact]
public void CanParseSingleMarkerWithParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = "Value" })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
var parameters = deserializedDescriptor.Parameters.ToDictionary();
Assert.Single(parameters);
Assert.Contains("Parameter", parameters.Keys);
Assert.Equal("Value", parameters["Parameter"]);
}
[Fact]
public void CanParseSingleMarkerWithNullParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["Parameter"] = null })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
var deserializedDescriptor = Assert.Single(descriptors);
Assert.Equal(typeof(TestComponent).FullName, deserializedDescriptor.ComponentType.FullName);
Assert.Equal(0, deserializedDescriptor.Sequence);
var parameters = deserializedDescriptor.Parameters.ToDictionary();
Assert.Single(parameters);
Assert.Contains("Parameter", parameters.Keys);
Assert.Null(parameters["Parameter"]);
}
[Fact]
public void CanParseMultipleMarkers()
{
@ -60,6 +100,65 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
Assert.Equal(1, secondDescriptor.Sequence);
}
[Fact]
public void CanParseMultipleMarkersWithParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
(typeof(TestComponent), new Dictionary<string, object> { ["Second"] = null })));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
var firstDescriptor = descriptors[0];
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);
var firstParameters = firstDescriptor.Parameters.ToDictionary();
Assert.Single(firstParameters);
Assert.Contains("First", firstParameters.Keys);
Assert.Equal("Value", firstParameters["First"]);
var secondDescriptor = descriptors[1];
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(1, secondDescriptor.Sequence);
var secondParameters = secondDescriptor.Parameters.ToDictionary();
Assert.Single(secondParameters);
Assert.Contains("Second", secondParameters.Keys);
Assert.Null(secondParameters["Second"]);
}
[Fact]
public void CanParseMultipleMarkersWithAndWithoutParameters()
{
// Arrange
var markers = SerializeMarkers(CreateMarkers(
(typeof(TestComponent), new Dictionary<string, object> { ["First"] = "Value" }),
(typeof(TestComponent), null)));
var serverComponentDeserializer = CreateServerComponentDeserializer();
// Act & assert
Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors));
Assert.Equal(2, descriptors.Count);
var firstDescriptor = descriptors[0];
Assert.Equal(typeof(TestComponent).FullName, firstDescriptor.ComponentType.FullName);
Assert.Equal(0, firstDescriptor.Sequence);
var firstParameters = firstDescriptor.Parameters.ToDictionary();
Assert.Single(firstParameters);
Assert.Contains("First", firstParameters.Keys);
Assert.Equal("Value", firstParameters["First"]);
var secondDescriptor = descriptors[1];
Assert.Equal(typeof(TestComponent).FullName, secondDescriptor.ComponentType.FullName);
Assert.Equal(1, secondDescriptor.Sequence);
Assert.Empty(secondDescriptor.Parameters.ToDictionary());
}
[Fact]
public void DoesNotParseOutOfOrderMarkers()
{
@ -213,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
private string SerializeComponent(string assembly, string type) =>
JsonSerializer.Serialize(
new ServerComponent(0, assembly, type, Guid.NewGuid()),
new ServerComponent(0, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
ServerComponentSerializationSettings.JsonSerializationOptions);
private ServerComponentDeserializer CreateServerComponentDeserializer()
@ -221,7 +320,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
return new ServerComponentDeserializer(
_ephemeralDataProtectionProvider,
NullLogger<ServerComponentDeserializer>.Instance,
new ServerComponentTypeCache());
new ServerComponentTypeCache(),
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
}
private string SerializeMarkers(ServerComponentMarker[] markers) =>
@ -233,7 +333,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], false);
markers[i] = serializer.SerializeInvocation(_invocationSequence, types[i], ParameterView.Empty, false);
}
return markers;
}
private ServerComponentMarker[] CreateMarkers(params (Type, Dictionary<string,object>)[] types)
{
var serializer = new ServerComponentSerializer(_ephemeralDataProtectionProvider);
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
var (type, parameters) = types[i];
markers[i] = serializer.SerializeInvocation(
_invocationSequence,
type,
parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(parameters),
false);
}
return markers;
@ -245,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
var markers = new ServerComponentMarker[types.Length];
for (var i = 0; i < types.Length; i++)
{
markers[i] = serializer.SerializeInvocation(sequence, types[i], false);
markers[i] = serializer.SerializeInvocation(sequence, types[i], ParameterView.Empty, false);
}
return markers;

View File

@ -52,6 +52,7 @@
<ItemGroup>
<!-- Shared descriptor infrastructure with MVC -->
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
</ItemGroup>

View File

@ -68,9 +68,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(4, greets.Length); // 1 statically rendered + 3 prerendered
Assert.Equal(5, greets.Length); // 1 statically rendered + 3 prerendered + 1 server prerendered
Assert.Single(greets, "Hello John");
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 prerendered
Assert.Single(greets, "Hello Abraham");
Assert.Equal(3, greets.Where(g => string.Equals("Hello", g)).Count()); // 3 server prerendered without parameters
var content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
var markers = ReadMarkers(content);
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
@ -84,6 +85,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
true,
false,
true,
false,
true
};
Assert.Equal(expectedComponentSequence, componentSequence);
@ -93,6 +96,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Exists(By.CssSelector("h3.interactive"));
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
Assert.Equal(7, updatedGreets.Where(g => string.Equals("Hello Alfred", g)).Count());
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Albert", g)));
Assert.Single(updatedGreets.Where(g => string.Equals("Hello Abraham", g)));
}
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)

View File

@ -9,7 +9,7 @@
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
if (firstRender && Name == null)
{
Name = "Alfred";
interactive = "interactive";

View File

@ -27,6 +27,10 @@
<p>Some content after</p>
</div>
</div>
<div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" }))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" }))
</div>
</div>
@*

View File

@ -94,11 +94,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Prerendering server components with parameters is not supported.");
}
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
@ -106,6 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
@ -121,14 +117,9 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
if (parametersCollection.GetEnumerator().MoveNext())
{
throw new InvalidOperationException("Server components with parameters are not supported.");
}
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, prerendered: false);
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}

View File

@ -34,6 +34,7 @@
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
</ItemGroup>
</Project>

View File

@ -19,22 +19,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, bool prerendered)
public ServerComponentMarker SerializeInvocation(ServerComponentInvocationSequence invocationId, Type type, ParameterView parameters, bool prerendered)
{
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type);
var (sequence, serverComponent) = CreateSerializedServerComponent(invocationId, type, parameters);
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
}
private (int sequence, string payload) CreateSerializedServerComponent(
ServerComponentInvocationSequence invocationId,
Type rootComponent)
Type rootComponent,
ParameterView parameters)
{
var sequence = invocationId.Next();
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
var serverComponent = new ServerComponent(
sequence,
rootComponent.Assembly.GetName().Name,
rootComponent.FullName,
definitions,
values,
invocationId.Value);
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);

View File

@ -188,25 +188,208 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Equal("<p>Hello Steve!</p>", content);
}
[Theory]
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
RenderMode renderMode,
string expectedMessage)
[Fact]
public async Task CanRender_ComponentWithParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act & Assert
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
renderMode,
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = "Steve"
}));
Assert.Equal(expectedMessage, result.Message);
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
}
[Fact]
public async Task CanRender_ComponentWithNullParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern);
// Assert
Assert.True(match.Success);
var marker = JsonSerializer.Deserialize<ServerComponentMarker>(match.Groups[1].Value, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, marker.Sequence);
Assert.Null(marker.PrerenderId);
Assert.NotNull(marker.Descriptor);
Assert.Equal("server", marker.Type);
var unprotectedServerComponent = protector.Unprotect(marker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
Assert.NotNull(serverComponent.ParameterDefinitions);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);;
Assert.Null(value);
}
[Fact]
public async Task CanPrerender_ComponentWithParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Equal("System.String", parameterDefinition.TypeName);
Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
var rawValue = Assert.IsType<JsonElement>(value);
Assert.Equal("Daniel", rawValue.GetString());
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello Daniel!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Sequence);
Assert.Null(epilogueMarker.Descriptor);
Assert.Null(epilogueMarker.Type);
}
[Fact]
public async Task CanPrerender_ComponentWithNullParameters_ServerMode()
{
// Arrange
var helper = CreateHelper();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector();
// Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.ServerPrerendered,
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert
Assert.True(match.Success);
var preamble = match.Groups["preamble"].Value;
var preambleMarker = JsonSerializer.Deserialize<ServerComponentMarker>(preamble, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(0, preambleMarker.Sequence);
Assert.NotNull(preambleMarker.PrerenderId);
Assert.NotNull(preambleMarker.Descriptor);
Assert.Equal("server", preambleMarker.Type);
var unprotectedServerComponent = protector.Unprotect(preambleMarker.Descriptor);
var serverComponent = JsonSerializer.Deserialize<ServerComponent>(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.NotEqual(default, serverComponent);
Assert.Equal(0, serverComponent.Sequence);
Assert.Equal(typeof(GreetingComponent).Assembly.GetName().Name, serverComponent.AssemblyName);
Assert.Equal(typeof(GreetingComponent).FullName, serverComponent.TypeName);
Assert.NotEqual(Guid.Empty, serverComponent.InvocationId);
Assert.NotNull(serverComponent.ParameterDefinitions);
var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions);
Assert.Equal("Name", parameterDefinition.Name);
Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);
Assert.Null(value);
var prerenderedContent = match.Groups["content"].Value;
Assert.Equal("<p>Hello (null)!</p>", prerenderedContent);
var epilogue = match.Groups["epilogue"].Value;
var epilogueMarker = JsonSerializer.Deserialize<ServerComponentMarker>(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions);
Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId);
Assert.Null(epilogueMarker.Sequence);
Assert.Null(epilogueMarker.Descriptor);
Assert.Null(epilogueMarker.Type);
}
[Fact]
@ -547,7 +730,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var s = 0;
base.BuildRenderTree(builder);
builder.OpenElement(s++, "p");
builder.AddContent(s++, $"Hello {Name}!");
builder.AddContent(s++, $"Hello {Name ?? ("(null)")}!");
builder.CloseElement();
}
}

View File

@ -0,0 +1,35 @@
// 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;
namespace Microsoft.AspNetCore.Components
{
internal struct ComponentParameter
{
public string Name { get; set; }
public string TypeName { get; set; }
public string Assembly { get; set; }
public static (IList<ComponentParameter> parameterDefinitions, IList<object> parameterValues) FromParameterView(ParameterView parameters)
{
var parameterDefinitions = new List<ComponentParameter>();
var parameterValues = new List<object>();
foreach (var kvp in parameters)
{
var valueType = kvp.Value?.GetType();
parameterDefinitions.Add(new ComponentParameter
{
Name = kvp.Name,
TypeName = valueType?.FullName,
Assembly = valueType?.Assembly?.GetName()?.Name
});
parameterValues.Add(kvp.Value);
}
return (parameterDefinitions, parameterValues);
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.AspNetCore.Components
{
@ -14,8 +15,11 @@ namespace Microsoft.AspNetCore.Components
int sequence,
string assemblyName,
string typeName,
IList<ComponentParameter> parametersDefinitions,
IList<object> parameterValues,
Guid invocationId) =>
(Sequence, AssemblyName, TypeName, InvocationId) = (sequence, assemblyName, typeName, invocationId);
(Sequence, AssemblyName, TypeName, ParameterDefinitions, ParameterValues, InvocationId) =
(sequence, assemblyName, typeName, parametersDefinitions, parameterValues, invocationId);
// The order in which this component was rendered
public int Sequence { get; set; }
@ -26,6 +30,12 @@ namespace Microsoft.AspNetCore.Components
// The type name of the component.
public string TypeName { get; set; }
// The definition for the parameters for the component.
public IList<ComponentParameter> ParameterDefinitions { get; set; }
// The values for the parameters for the component.
public IList<object> ParameterValues { get; set; }
// An id that uniquely identifies all components generated as part of a single HTTP response.
public Guid InvocationId { get; set; }
}