Support parameters on server-side rendered components
Fixes https://github.com/aspnet/AspNetCore/issues/14433
This commit is contained in:
parent
cc368c8e08
commit
6bc4d27bfa
|
|
@ -111,8 +111,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var count = Descriptors.Count;
|
var count = Descriptors.Count;
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
var (componentType, sequence) = Descriptors[i];
|
var (componentType, parameters, sequence) = Descriptors[i];
|
||||||
await Renderer.AddComponentAsync(componentType, sequence.ToString());
|
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.InitializationSucceeded(_logger);
|
Log.InitializationSucceeded(_logger);
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,11 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
{
|
{
|
||||||
public Type ComponentType { get; set; }
|
public Type ComponentType { get; set; }
|
||||||
|
|
||||||
|
public ParameterView Parameters { get; set; }
|
||||||
|
|
||||||
public int Sequence { get; set; }
|
public int Sequence { get; set; }
|
||||||
|
|
||||||
public void Deconstruct(out Type componentType, out int sequence)
|
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
|
||||||
{
|
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
|
||||||
componentType = ComponentType;
|
|
||||||
sequence = Sequence;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -64,6 +64,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return RenderRootComponentAsync(componentId);
|
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()
|
protected override void ProcessPendingRender()
|
||||||
{
|
{
|
||||||
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
|
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
// 'sequence' indicates the order in which this component got rendered on the server.
|
// 'sequence' indicates the order in which this component got rendered on the server.
|
||||||
// 'assemblyName' the assembly name for the rendered component.
|
// 'assemblyName' the assembly name for the rendered component.
|
||||||
// 'type' the full type 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.
|
// '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>>"}))
|
// 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:
|
// Serialization:
|
||||||
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
|
// 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 IDataProtector _dataProtector;
|
||||||
private readonly ILogger<ServerComponentDeserializer> _logger;
|
private readonly ILogger<ServerComponentDeserializer> _logger;
|
||||||
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
private readonly ServerComponentTypeCache _rootComponentTypeCache;
|
||||||
|
private readonly ComponentParameterDeserializer _parametersDeserializer;
|
||||||
|
|
||||||
public ServerComponentDeserializer(
|
public ServerComponentDeserializer(
|
||||||
IDataProtectionProvider dataProtectionProvider,
|
IDataProtectionProvider dataProtectionProvider,
|
||||||
ILogger<ServerComponentDeserializer> logger,
|
ILogger<ServerComponentDeserializer> logger,
|
||||||
ServerComponentTypeCache rootComponentTypeCache)
|
ServerComponentTypeCache rootComponentTypeCache,
|
||||||
|
ComponentParameterDeserializer parametersDeserializer)
|
||||||
{
|
{
|
||||||
// When we protect the data we use a time-limited data protector with the
|
// When we protect the data we use a time-limited data protector with the
|
||||||
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
|
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
|
||||||
|
|
@ -74,6 +80,7 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_rootComponentTypeCache = rootComponentTypeCache;
|
_rootComponentTypeCache = rootComponentTypeCache;
|
||||||
|
_parametersDeserializer = parametersDeserializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
|
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
|
||||||
|
|
@ -176,9 +183,16 @@ namespace Microsoft.AspNetCore.Components.Server
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
|
||||||
|
{
|
||||||
|
// TryDeserializeParameters does appropriate logging.
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
var componentDescriptor = new ComponentDescriptor
|
var componentDescriptor = new ComponentDescriptor
|
||||||
{
|
{
|
||||||
ComponentType = componentType,
|
ComponentType = componentType,
|
||||||
|
Parameters = parameters,
|
||||||
Sequence = serverComponent.Sequence
|
Sequence = serverComponent.Sequence
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
services.TryAddSingleton<CircuitFactory>();
|
services.TryAddSingleton<CircuitFactory>();
|
||||||
services.TryAddSingleton<ServerComponentDeserializer>();
|
services.TryAddSingleton<ServerComponentDeserializer>();
|
||||||
services.TryAddSingleton<ServerComponentTypeCache>();
|
services.TryAddSingleton<ServerComponentTypeCache>();
|
||||||
|
services.TryAddSingleton<ComponentParameterDeserializer>();
|
||||||
|
services.TryAddSingleton<ComponentParametersTypeCache>();
|
||||||
services.TryAddSingleton<CircuitIdFactory>();
|
services.TryAddSingleton<CircuitIdFactory>();
|
||||||
|
|
||||||
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
|
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
|
|
||||||
<!-- Shared descriptor infrastructure with MVC -->
|
<!-- Shared descriptor infrastructure with MVC -->
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<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\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -40,6 +41,45 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
Assert.Equal(0, deserializedDescriptor.Sequence);
|
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]
|
[Fact]
|
||||||
public void CanParseMultipleMarkers()
|
public void CanParseMultipleMarkers()
|
||||||
{
|
{
|
||||||
|
|
@ -60,6 +100,65 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
Assert.Equal(1, secondDescriptor.Sequence);
|
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]
|
[Fact]
|
||||||
public void DoesNotParseOutOfOrderMarkers()
|
public void DoesNotParseOutOfOrderMarkers()
|
||||||
{
|
{
|
||||||
|
|
@ -213,7 +312,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
|
|
||||||
private string SerializeComponent(string assembly, string type) =>
|
private string SerializeComponent(string assembly, string type) =>
|
||||||
JsonSerializer.Serialize(
|
JsonSerializer.Serialize(
|
||||||
new ServerComponent(0, assembly, type, Guid.NewGuid()),
|
new ServerComponent(0, assembly, type, Array.Empty<ComponentParameter>(), Array.Empty<object>(), Guid.NewGuid()),
|
||||||
ServerComponentSerializationSettings.JsonSerializationOptions);
|
ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
||||||
private ServerComponentDeserializer CreateServerComponentDeserializer()
|
private ServerComponentDeserializer CreateServerComponentDeserializer()
|
||||||
|
|
@ -221,7 +320,8 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
return new ServerComponentDeserializer(
|
return new ServerComponentDeserializer(
|
||||||
_ephemeralDataProtectionProvider,
|
_ephemeralDataProtectionProvider,
|
||||||
NullLogger<ServerComponentDeserializer>.Instance,
|
NullLogger<ServerComponentDeserializer>.Instance,
|
||||||
new ServerComponentTypeCache());
|
new ServerComponentTypeCache(),
|
||||||
|
new ComponentParameterDeserializer(NullLogger<ComponentParameterDeserializer>.Instance, new ComponentParametersTypeCache()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SerializeMarkers(ServerComponentMarker[] markers) =>
|
private string SerializeMarkers(ServerComponentMarker[] markers) =>
|
||||||
|
|
@ -233,7 +333,24 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var markers = new ServerComponentMarker[types.Length];
|
var markers = new ServerComponentMarker[types.Length];
|
||||||
for (var i = 0; i < types.Length; i++)
|
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;
|
return markers;
|
||||||
|
|
@ -245,7 +362,7 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
|
||||||
var markers = new ServerComponentMarker[types.Length];
|
var markers = new ServerComponentMarker[types.Length];
|
||||||
for (var i = 0; i < types.Length; i++)
|
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;
|
return markers;
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- Shared descriptor infrastructure with MVC -->
|
<!-- Shared descriptor infrastructure with MVC -->
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<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\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,10 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
|
|
||||||
var greets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
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.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 content = Browser.FindElement(By.Id("test-container")).GetAttribute("innerHTML");
|
||||||
var markers = ReadMarkers(content);
|
var markers = ReadMarkers(content);
|
||||||
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
var componentSequence = markers.Select(m => m.Item1.PrerenderId != null).ToArray();
|
||||||
|
|
@ -84,6 +85,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
|
true
|
||||||
};
|
};
|
||||||
Assert.Equal(expectedComponentSequence, componentSequence);
|
Assert.Equal(expectedComponentSequence, componentSequence);
|
||||||
|
|
||||||
|
|
@ -93,6 +96,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
|
||||||
Browser.Exists(By.CssSelector("h3.interactive"));
|
Browser.Exists(By.CssSelector("h3.interactive"));
|
||||||
var updatedGreets = Browser.FindElements(By.CssSelector(".greet-wrapper .greet")).Select(e => e.Text).ToArray();
|
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.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)
|
private (ServerComponentMarker, ServerComponentMarker)[] ReadMarkers(string content)
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
protected override void OnAfterRender(bool firstRender)
|
protected override void OnAfterRender(bool firstRender)
|
||||||
{
|
{
|
||||||
if (firstRender)
|
if (firstRender && Name == null)
|
||||||
{
|
{
|
||||||
Name = "Alfred";
|
Name = "Alfred";
|
||||||
interactive = "interactive";
|
interactive = "interactive";
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@
|
||||||
<p>Some content after</p>
|
<p>Some content after</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
@*
|
@*
|
||||||
|
|
|
||||||
|
|
@ -94,11 +94,6 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
|
|
||||||
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
|
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 serviceProvider = context.RequestServices;
|
||||||
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
|
||||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
||||||
|
|
@ -106,6 +101,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
var currentInvocation = invocationSerializer.SerializeInvocation(
|
var currentInvocation = invocationSerializer.SerializeInvocation(
|
||||||
invocationId,
|
invocationId,
|
||||||
type,
|
type,
|
||||||
|
parametersCollection,
|
||||||
prerendered: true);
|
prerendered: true);
|
||||||
|
|
||||||
var result = await prerenderer.PrerenderComponentAsync(
|
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)
|
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 serviceProvider = context.RequestServices;
|
||||||
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
|
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));
|
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
|
||||||
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
|
||||||
|
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -19,22 +19,27 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
.ToTimeLimitedDataProtector();
|
.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);
|
return prerendered ? ServerComponentMarker.Prerendered(sequence, serverComponent) : ServerComponentMarker.NonPrerendered(sequence, serverComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int sequence, string payload) CreateSerializedServerComponent(
|
private (int sequence, string payload) CreateSerializedServerComponent(
|
||||||
ServerComponentInvocationSequence invocationId,
|
ServerComponentInvocationSequence invocationId,
|
||||||
Type rootComponent)
|
Type rootComponent,
|
||||||
|
ParameterView parameters)
|
||||||
{
|
{
|
||||||
var sequence = invocationId.Next();
|
var sequence = invocationId.Next();
|
||||||
|
|
||||||
|
var (definitions, values) = ComponentParameter.FromParameterView(parameters);
|
||||||
|
|
||||||
var serverComponent = new ServerComponent(
|
var serverComponent = new ServerComponent(
|
||||||
sequence,
|
sequence,
|
||||||
rootComponent.Assembly.GetName().Name,
|
rootComponent.Assembly.GetName().Name,
|
||||||
rootComponent.FullName,
|
rootComponent.FullName,
|
||||||
|
definitions,
|
||||||
|
values,
|
||||||
invocationId.Value);
|
invocationId.Value);
|
||||||
|
|
||||||
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
var serializedServerComponent = JsonSerializer.Serialize(serverComponent, ServerComponentSerializationSettings.JsonSerializationOptions);
|
||||||
|
|
|
||||||
|
|
@ -188,25 +188,208 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
Assert.Equal("<p>Hello Steve!</p>", content);
|
Assert.Equal("<p>Hello Steve!</p>", content);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(RenderMode.Server, "Server components with parameters are not supported.")]
|
public async Task CanRender_ComponentWithParameters_ServerMode()
|
||||||
[InlineData(RenderMode.ServerPrerendered, "Prerendering server components with parameters is not supported.")]
|
|
||||||
public async Task ComponentWithParametersObject_ThrowsInvalidOperationExceptionForServerRenderModes(
|
|
||||||
RenderMode renderMode,
|
|
||||||
string expectedMessage)
|
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var helper = CreateHelper();
|
var helper = CreateHelper();
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
|
||||||
|
.ToTimeLimitedDataProtector();
|
||||||
|
|
||||||
// Act & Assert
|
// Act
|
||||||
var result = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<GreetingComponent>(
|
var result = await helper.RenderComponentAsync<GreetingComponent>(
|
||||||
renderMode,
|
RenderMode.Server,
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Name = "Steve"
|
Name = "Daniel"
|
||||||
}));
|
});
|
||||||
Assert.Equal(expectedMessage, result.Message);
|
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]
|
[Fact]
|
||||||
|
|
@ -547,7 +730,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
|
||||||
var s = 0;
|
var s = 0;
|
||||||
base.BuildRenderTree(builder);
|
base.BuildRenderTree(builder);
|
||||||
builder.OpenElement(s++, "p");
|
builder.OpenElement(s++, "p");
|
||||||
builder.AddContent(s++, $"Hello {Name}!");
|
builder.AddContent(s++, $"Hello {Name ?? ("(null)")}!");
|
||||||
builder.CloseElement();
|
builder.CloseElement();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components
|
namespace Microsoft.AspNetCore.Components
|
||||||
{
|
{
|
||||||
|
|
@ -14,8 +15,11 @@ namespace Microsoft.AspNetCore.Components
|
||||||
int sequence,
|
int sequence,
|
||||||
string assemblyName,
|
string assemblyName,
|
||||||
string typeName,
|
string typeName,
|
||||||
|
IList<ComponentParameter> parametersDefinitions,
|
||||||
|
IList<object> parameterValues,
|
||||||
Guid invocationId) =>
|
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
|
// The order in which this component was rendered
|
||||||
public int Sequence { get; set; }
|
public int Sequence { get; set; }
|
||||||
|
|
@ -26,6 +30,12 @@ namespace Microsoft.AspNetCore.Components
|
||||||
// The type name of the component.
|
// The type name of the component.
|
||||||
public string TypeName { get; set; }
|
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.
|
// An id that uniquely identifies all components generated as part of a single HTTP response.
|
||||||
public Guid InvocationId { get; set; }
|
public Guid InvocationId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue