Skip to content

Commit 6bc4d27

Browse files
javiercnmkArtakMSFT
authored andcommitted
Support parameters on server-side rendered components
Fixes #14433
1 parent cc368c8 commit 6bc4d27

19 files changed

+675
-43
lines changed

src/Components/Server/src/Circuits/CircuitHost.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ public Task InitializeAsync(CancellationToken cancellationToken)
111111
var count = Descriptors.Count;
112112
for (var i = 0; i < count; i++)
113113
{
114-
var (componentType, sequence) = Descriptors[i];
115-
await Renderer.AddComponentAsync(componentType, sequence.ToString());
114+
var (componentType, parameters, sequence) = Descriptors[i];
115+
await Renderer.AddComponentAsync(componentType, parameters, sequence.ToString());
116116
}
117117

118118
Log.InitializationSucceeded(_logger);

src/Components/Server/src/Circuits/ComponentDescriptor.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@ internal class ComponentDescriptor
99
{
1010
public Type ComponentType { get; set; }
1111

12+
public ParameterView Parameters { get; set; }
13+
1214
public int Sequence { get; set; }
1315

14-
public void Deconstruct(out Type componentType, out int sequence)
15-
{
16-
componentType = ComponentType;
17-
sequence = Sequence;
18-
}
16+
public void Deconstruct(out Type componentType, out ParameterView parameters, out int sequence) =>
17+
(componentType, sequence, parameters) = (ComponentType, Sequence, Parameters);
1918
}
2019
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Text;
7+
using System.Text.Json;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Microsoft.AspNetCore.Components.Server
11+
{
12+
internal class ComponentParameterDeserializer
13+
{
14+
private readonly ILogger<ComponentParameterDeserializer> _logger;
15+
private readonly ComponentParametersTypeCache _parametersCache;
16+
17+
public ComponentParameterDeserializer(
18+
ILogger<ComponentParameterDeserializer> logger,
19+
ComponentParametersTypeCache parametersCache)
20+
{
21+
_logger = logger;
22+
_parametersCache = parametersCache;
23+
}
24+
25+
public bool TryDeserializeParameters(IList<ComponentParameter> parametersDefinitions, IList<object> parameterValues, out ParameterView parameters)
26+
{
27+
parameters = default;
28+
var parametersDictionary = new Dictionary<string, object>();
29+
30+
if (parameterValues.Count != parametersDefinitions.Count)
31+
{
32+
// Mismatched number of definition/parameter values.
33+
Log.MismatchedParameterAndDefinitions(_logger, parametersDefinitions.Count, parameterValues.Count);
34+
return false;
35+
}
36+
37+
for (var i = 0; i < parametersDefinitions.Count; i++)
38+
{
39+
var definition = parametersDefinitions[i];
40+
if (definition.Name == null)
41+
{
42+
Log.MissingParameterDefinitionName(_logger);
43+
return false;
44+
}
45+
46+
if (definition.TypeName == null && definition.Assembly == null)
47+
{
48+
parametersDictionary.Add(definition.Name, null);
49+
}
50+
else if (definition.TypeName == null || definition.Assembly == null)
51+
{
52+
Log.IncompleteParameterDefinition(_logger, definition.Name, definition.TypeName, definition.Assembly);
53+
return false;
54+
}
55+
else
56+
{
57+
var parameterType = _parametersCache.GetParameterType(definition.Assembly, definition.TypeName);
58+
if (parameterType == null)
59+
{
60+
Log.InvalidParameterType(_logger, definition.Name, definition.Assembly, definition.TypeName);
61+
return false;
62+
}
63+
try
64+
{
65+
// At this point we know the parameter is not null, as we don't serialize the type name or the assembly name
66+
// for null parameters.
67+
var value = (JsonElement)parameterValues[i];
68+
var parameterValue = JsonSerializer.Deserialize(
69+
value.GetRawText(),
70+
parameterType,
71+
ServerComponentSerializationSettings.JsonSerializationOptions);
72+
73+
parametersDictionary.Add(definition.Name, parameterValue);
74+
}
75+
catch (Exception e)
76+
{
77+
Log.InvalidParameterValue(_logger, definition.Name, definition.TypeName, definition.Assembly, e);
78+
return false;
79+
}
80+
}
81+
}
82+
83+
parameters = ParameterView.FromDictionary(parametersDictionary);
84+
return true;
85+
}
86+
87+
private ComponentParameter[] GetParameterDefinitions(string parametersDefinitions)
88+
{
89+
try
90+
{
91+
return JsonSerializer.Deserialize<ComponentParameter[]>(parametersDefinitions, ServerComponentSerializationSettings.JsonSerializationOptions);
92+
}
93+
catch (Exception e)
94+
{
95+
Log.FailedToParseParameterDefinitions(_logger, e);
96+
return null;
97+
}
98+
}
99+
100+
private JsonDocument GetParameterValues(string parameterValues)
101+
{
102+
try
103+
{
104+
return JsonDocument.Parse(parameterValues);
105+
}
106+
catch (Exception e)
107+
{
108+
Log.FailedToParseParameterValues(_logger, e);
109+
return null;
110+
}
111+
}
112+
113+
private static class Log
114+
{
115+
private static readonly Action<ILogger, Exception> _parameterValuesInvalidFormat =
116+
LoggerMessage.Define(
117+
LogLevel.Debug,
118+
new EventId(1, "ParameterValuesInvalidFormat"),
119+
"Parameter values must be an array.");
120+
121+
private static readonly Action<ILogger, string, string, string, Exception> _incompleteParameterDefinition =
122+
LoggerMessage.Define<string, string, string>(
123+
LogLevel.Debug,
124+
new EventId(2, "IncompleteParameterDefinition"),
125+
"The parameter definition for '{ParameterName}' is incomplete: Type='{TypeName}' Assembly='{Assembly}'.");
126+
127+
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterType =
128+
LoggerMessage.Define<string, string, string>(
129+
LogLevel.Debug,
130+
new EventId(3, "InvalidParameterType"),
131+
"The parameter '{ParameterName} with type '{TypeName}' in assembly '{Assembly}' could not be found.");
132+
133+
private static readonly Action<ILogger, string, string, string, Exception> _invalidParameterValue =
134+
LoggerMessage.Define<string, string, string>(
135+
LogLevel.Debug,
136+
new EventId(4, "InvalidParameterValue"),
137+
"Could not parse the parameter value for parameter '{Name}' of type '{TypeName}' and assembly '{Assembly}'.");
138+
139+
private static readonly Action<ILogger, Exception> _failedToParseParameterDefinitions =
140+
LoggerMessage.Define(
141+
LogLevel.Debug,
142+
new EventId(5, "FailedToParseParameterDefinitions"),
143+
"Failed to parse the parameter definitions.");
144+
145+
private static readonly Action<ILogger, Exception> _failedToParseParameterValues =
146+
LoggerMessage.Define(
147+
LogLevel.Debug,
148+
new EventId(6, "FailedToParseParameterValues"),
149+
"Failed to parse the parameter values.");
150+
151+
private static readonly Action<ILogger, int, int, Exception> _mismatchedParameterAndDefinitions =
152+
LoggerMessage.Define<int, int>(
153+
LogLevel.Debug,
154+
new EventId(7, "MismatchedParameterAndDefinitions"),
155+
"The number of parameter definitions '{DescriptorsLength}' does not match the number parameter values '{ValuesLength}'.");
156+
157+
private static readonly Action<ILogger, Exception> _missingParameterDefinitionName =
158+
LoggerMessage.Define(
159+
LogLevel.Debug,
160+
new EventId(8, "MissingParameterDefinitionName"),
161+
"The name is missing in a parameter definition.");
162+
163+
internal static void ParameterValuesInvalidFormat(ILogger<ComponentParameterDeserializer> logger) =>
164+
_parameterValuesInvalidFormat(logger, null);
165+
166+
internal static void IncompleteParameterDefinition(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly) =>
167+
_incompleteParameterDefinition(logger, name, typeName, assembly, null);
168+
169+
internal static void InvalidParameterType(ILogger<ComponentParameterDeserializer> logger, string name, string assembly, string typeName) =>
170+
_invalidParameterType(logger, name, assembly, typeName, null);
171+
172+
internal static void InvalidParameterValue(ILogger<ComponentParameterDeserializer> logger, string name, string typeName, string assembly, Exception e) =>
173+
_invalidParameterValue(logger, name, typeName, assembly,e);
174+
175+
internal static void FailedToParseParameterDefinitions(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
176+
_failedToParseParameterDefinitions(logger, e);
177+
178+
internal static void FailedToParseParameterValues(ILogger<ComponentParameterDeserializer> logger, Exception e) =>
179+
_failedToParseParameterValues(logger, e);
180+
181+
internal static void MismatchedParameterAndDefinitions(ILogger<ComponentParameterDeserializer> logger, int definitionsLength, int valuesLength) =>
182+
_mismatchedParameterAndDefinitions(logger, definitionsLength, valuesLength, null);
183+
184+
internal static void MissingParameterDefinitionName(ILogger<ComponentParameterDeserializer> logger) =>
185+
_missingParameterDefinitionName(logger, null);
186+
}
187+
}
188+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Concurrent;
6+
using System.Linq;
7+
using System.Reflection;
8+
9+
namespace Microsoft.AspNetCore.Components
10+
{
11+
internal class ComponentParametersTypeCache
12+
{
13+
private readonly ConcurrentDictionary<Key, Type> _typeToKeyLookUp = new ConcurrentDictionary<Key, Type>();
14+
15+
public Type GetParameterType(string assembly, string type)
16+
{
17+
var key = new Key(assembly, type);
18+
if (_typeToKeyLookUp.TryGetValue(key, out var resolvedType))
19+
{
20+
return resolvedType;
21+
}
22+
else
23+
{
24+
return _typeToKeyLookUp.GetOrAdd(key, ResolveType, AppDomain.CurrentDomain.GetAssemblies());
25+
}
26+
}
27+
28+
private static Type ResolveType(Key key, Assembly[] assemblies)
29+
{
30+
var assembly = assemblies
31+
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
32+
33+
if (assembly == null)
34+
{
35+
return null;
36+
}
37+
38+
return assembly.GetType(key.Type, throwOnError: false, ignoreCase: false);
39+
}
40+
41+
private struct Key : IEquatable<Key>
42+
{
43+
public Key(string assembly, string type) =>
44+
(Assembly, Type) = (assembly, type);
45+
46+
public string Assembly { get; set; }
47+
48+
public string Type { get; set; }
49+
50+
public override bool Equals(object obj) => Equals((Key)obj);
51+
52+
public bool Equals(Key other) => string.Equals(Assembly, other.Assembly, StringComparison.Ordinal) &&
53+
string.Equals(Type, other.Type, StringComparison.Ordinal);
54+
55+
public override int GetHashCode() => HashCode.Combine(Assembly, Type);
56+
}
57+
}
58+
}

src/Components/Server/src/Circuits/RemoteRenderer.cs

+18
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ public Task AddComponentAsync(Type componentType, string domElementSelector)
6464
return RenderRootComponentAsync(componentId);
6565
}
6666

67+
/// <summary>
68+
/// Associates the <see cref="IComponent"/> with the <see cref="RemoteRenderer"/>,
69+
/// causing it to be displayed in the specified DOM element.
70+
/// </summary>
71+
/// <param name="componentType">The type of the component.</param>
72+
/// <param name="parameters">The parameters for the component.</param>
73+
/// <param name="domElementSelector">A CSS selector that uniquely identifies a DOM element.</param>
74+
public Task AddComponentAsync(Type componentType, ParameterView parameters, string domElementSelector)
75+
{
76+
var component = InstantiateComponent(componentType);
77+
var componentId = AssignRootComponentId(component);
78+
79+
var attachComponentTask = _client.SendAsync("JS.AttachComponent", componentId, domElementSelector);
80+
CaptureAsyncExceptions(attachComponentTask);
81+
82+
return RenderRootComponentAsync(componentId, parameters);
83+
}
84+
6785
protected override void ProcessPendingRender()
6886
{
6987
if (_unacknowledgedRenderBatches.Count >= _options.MaxBufferedUnacknowledgedRenderBatches)

src/Components/Server/src/Circuits/ServerComponentDeserializer.cs

+15-1
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,12 @@ namespace Microsoft.AspNetCore.Components.Server
2525
// 'sequence' indicates the order in which this component got rendered on the server.
2626
// 'assemblyName' the assembly name for the rendered component.
2727
// 'type' the full type name for the rendered component.
28+
// 'parameterDefinitions' a JSON serialized array that contains the definitions for the parameters including their names and types and assemblies.
29+
// 'parameterValues' a JSON serialized array containing the parameter values.
2830
// 'invocationId' a random string that matches all components rendered by as part of a single HTTP response.
2931
// For example: base64(dataprotection({ "sequence": 1, "assemblyName": "Microsoft.AspNetCore.Components", "type":"Microsoft.AspNetCore.Components.Routing.Router", "invocationId": "<<guid>>"}))
32+
// With parameters
33+
// 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>>]}))
3034

3135
// Serialization:
3236
// For a given response, MVC renders one or more markers in sequence, including a descriptor for each rendered
@@ -55,11 +59,13 @@ internal class ServerComponentDeserializer
5559
private readonly IDataProtector _dataProtector;
5660
private readonly ILogger<ServerComponentDeserializer> _logger;
5761
private readonly ServerComponentTypeCache _rootComponentTypeCache;
62+
private readonly ComponentParameterDeserializer _parametersDeserializer;
5863

5964
public ServerComponentDeserializer(
6065
IDataProtectionProvider dataProtectionProvider,
6166
ILogger<ServerComponentDeserializer> logger,
62-
ServerComponentTypeCache rootComponentTypeCache)
67+
ServerComponentTypeCache rootComponentTypeCache,
68+
ComponentParameterDeserializer parametersDeserializer)
6369
{
6470
// When we protect the data we use a time-limited data protector with the
6571
// limits established in 'ServerComponentSerializationSettings.DataExpiration'
@@ -74,6 +80,7 @@ public ServerComponentDeserializer(
7480

7581
_logger = logger;
7682
_rootComponentTypeCache = rootComponentTypeCache;
83+
_parametersDeserializer = parametersDeserializer;
7784
}
7885

7986
public bool TryDeserializeComponentDescriptorCollection(string serializedComponentRecords, out List<ComponentDescriptor> descriptors)
@@ -176,9 +183,16 @@ public bool TryDeserializeComponentDescriptorCollection(string serializedCompone
176183
return default;
177184
}
178185

186+
if (!_parametersDeserializer.TryDeserializeParameters(serverComponent.ParameterDefinitions, serverComponent.ParameterValues, out var parameters))
187+
{
188+
// TryDeserializeParameters does appropriate logging.
189+
return default;
190+
}
191+
179192
var componentDescriptor = new ComponentDescriptor
180193
{
181194
ComponentType = componentType,
195+
Parameters = parameters,
182196
Sequence = serverComponent.Sequence
183197
};
184198

src/Components/Server/src/DependencyInjection/ComponentServiceCollectionExtensions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public static IServerSideBlazorBuilder AddServerSideBlazor(this IServiceCollecti
5858
services.TryAddSingleton<CircuitFactory>();
5959
services.TryAddSingleton<ServerComponentDeserializer>();
6060
services.TryAddSingleton<ServerComponentTypeCache>();
61+
services.TryAddSingleton<ComponentParameterDeserializer>();
62+
services.TryAddSingleton<ComponentParametersTypeCache>();
6163
services.TryAddSingleton<CircuitIdFactory>();
6264

6365
services.TryAddScoped(s => s.GetRequiredService<ICircuitAccessor>().Circuit);

src/Components/Server/src/Microsoft.AspNetCore.Components.Server.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070

7171
<!-- Shared descriptor infrastructure with MVC -->
7272
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponent.cs" />
73+
<Compile Include="$(RepoRoot)src\Shared\Components\ComponentParameter.cs" />
7374
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentSerializationSettings.cs" />
7475
<Compile Include="$(RepoRoot)src\Shared\Components\ServerComponentMarker.cs" />
7576
</ItemGroup>

0 commit comments

Comments
 (0)