diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 7fa40b2cd010..62151e6bdbaf 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -718,6 +718,133 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode Assert.Null(epilogueMarker.Type); } + [Fact] + public async Task CanRender_ClosedGenericComponent() + { + // Arrange + var httpContext = GetHttpContext(); + var writer = new StringWriter(); + + // Act + var parameters = ParameterView.FromDictionary(new Dictionary { { "Value", 42 } }); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent), null, parameters); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); + var content = writer.ToString(); + + // Assert + Assert.Equal("

Generic value: 42

", content); + } + + [Fact] + public async Task CanRender_ClosedGenericComponent_ServerMode() + { + // Arrange + var httpContext = GetHttpContext(); + var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) + .ToTimeLimitedDataProtector(); + + // Act + var parameters = ParameterView.FromDictionary(new Dictionary { { "Value", "TestString" } }); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent), new InteractiveServerRenderMode(false), parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); + var match = Regex.Match(content, ComponentPattern); + + // Assert + Assert.True(match.Success); + var marker = JsonSerializer.Deserialize(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(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(0, serverComponent.Sequence); + Assert.Equal(typeof(GenericComponent).Assembly.GetName().Name, serverComponent.AssemblyName); + Assert.Equal(typeof(GenericComponent).FullName, serverComponent.TypeName); + Assert.NotEqual(Guid.Empty, serverComponent.InvocationId); + + var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions); + Assert.Equal("Value", 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(value); + Assert.Equal("TestString", rawValue.GetString()); + } + + [Fact] + public async Task CanPrerender_ClosedGenericComponent_ServerMode() + { + // Arrange + var httpContext = GetHttpContext(); + var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) + .ToTimeLimitedDataProtector(); + + // Act + var parameters = ParameterView.FromDictionary(new Dictionary { { "Value", 123 } }); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent), RenderMode.InteractiveServer, parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); + var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); + + // Assert + Assert.True(match.Success); + var preamble = match.Groups["preamble"].Value; + var preambleMarker = JsonSerializer.Deserialize(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(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.NotEqual(default, serverComponent); + Assert.Equal(0, serverComponent.Sequence); + Assert.Equal(typeof(GenericComponent).Assembly.GetName().Name, serverComponent.AssemblyName); + Assert.Equal(typeof(GenericComponent).FullName, serverComponent.TypeName); + Assert.NotEqual(Guid.Empty, serverComponent.InvocationId); + + var prerenderedContent = match.Groups["content"].Value; + Assert.Equal("

Generic value: 123

", prerenderedContent); + + var epilogue = match.Groups["epilogue"].Value; + var epilogueMarker = JsonSerializer.Deserialize(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId); + } + + [Fact] + public async Task CanPrerender_ClosedGenericComponent_ClientMode() + { + // Arrange + var httpContext = GetHttpContext(); + var writer = new StringWriter(); + + // Act + var parameters = ParameterView.FromDictionary(new Dictionary { { "Value", 456 } }); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GenericComponent), RenderMode.InteractiveWebAssembly, parameters); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); + var content = writer.ToString(); + content = AssertAndStripWebAssemblyOptions(content); + var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); + + // Assert + Assert.True(match.Success); + var preamble = match.Groups["preamble"].Value; + var preambleMarker = JsonSerializer.Deserialize(preamble, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.NotNull(preambleMarker.PrerenderId); + Assert.Equal("webassembly", preambleMarker.Type); + Assert.Equal(typeof(GenericComponent).Assembly.GetName().Name, preambleMarker.Assembly); + Assert.Equal(typeof(GenericComponent).FullName, preambleMarker.TypeName); + + var prerenderedContent = match.Groups["content"].Value; + Assert.Equal("

Generic value: 456

", prerenderedContent); + + var epilogue = match.Groups["epilogue"].Value; + var epilogueMarker = JsonSerializer.Deserialize(epilogue, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(preambleMarker.PrerenderId, epilogueMarker.PrerenderId); + } + [Fact] public async Task ComponentWithInvalidRenderMode_Throws() { diff --git a/src/Components/Endpoints/test/TestComponents/GenericComponent.razor b/src/Components/Endpoints/test/TestComponents/GenericComponent.razor new file mode 100644 index 000000000000..d0fe347178d6 --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/GenericComponent.razor @@ -0,0 +1,6 @@ +@typeparam TValue + +

Generic value: @(Value?.ToString() ?? "(null)")

+@code { + [Parameter] public TValue Value { get; set; } +} diff --git a/src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs b/src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs index 0d04a3135dfe..1950bba84d1d 100644 --- a/src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs +++ b/src/Components/Server/test/Circuits/ServerComponentDeserializerTest.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using System.Text.Json; using Microsoft.AspNetCore.Components.Endpoints; using Microsoft.AspNetCore.DataProtection; @@ -75,6 +76,74 @@ public void CanParseSingleMarkerWithNullParameters() Assert.Null(parameters["Parameter"]); } + [Fact] + public void CanParseSingleMarkerForClosedGenericComponent() + { + // Arrange + var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent))); + var serverComponentDeserializer = CreateServerComponentDeserializer(); + + // Act & assert + Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors)); + var deserializedDescriptor = Assert.Single(descriptors); + Assert.Equal(typeof(GenericTestComponent).FullName, deserializedDescriptor.ComponentType.FullName); + Assert.Equal(0, deserializedDescriptor.Sequence); + } + + [Fact] + public void CanParseSingleMarkerForClosedGenericComponentWithStringTypeParameter() + { + // Arrange + var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent))); + var serverComponentDeserializer = CreateServerComponentDeserializer(); + + // Act & assert + Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors)); + var deserializedDescriptor = Assert.Single(descriptors); + Assert.Equal(typeof(GenericTestComponent).FullName, deserializedDescriptor.ComponentType.FullName); + Assert.Equal(0, deserializedDescriptor.Sequence); + } + + [Fact] + public void CanParseSingleMarkerForClosedGenericComponentWithParameters() + { + // Arrange + var markers = SerializeMarkers(CreateMarkers( + (typeof(GenericTestComponent), new Dictionary { ["Value"] = 42 }))); + var serverComponentDeserializer = CreateServerComponentDeserializer(); + + // Act & assert + Assert.True(serverComponentDeserializer.TryDeserializeComponentDescriptorCollection(markers, out var descriptors)); + var deserializedDescriptor = Assert.Single(descriptors); + Assert.Equal(typeof(GenericTestComponent).FullName, deserializedDescriptor.ComponentType.FullName); + Assert.Equal(0, deserializedDescriptor.Sequence); + + var parameters = deserializedDescriptor.Parameters.ToDictionary(); + Assert.Single(parameters); + Assert.Contains("Value", parameters.Keys); + Assert.Equal(42, Convert.ToInt32(parameters["Value"]!, CultureInfo.InvariantCulture)); + } + + [Fact] + public void CanParseMultipleMarkersForClosedGenericComponents() + { + // Arrange + var markers = SerializeMarkers(CreateMarkers(typeof(GenericTestComponent), typeof(GenericTestComponent))); + 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(GenericTestComponent).FullName, firstDescriptor.ComponentType.FullName); + Assert.Equal(0, firstDescriptor.Sequence); + + var secondDescriptor = descriptors[1]; + Assert.Equal(typeof(GenericTestComponent).FullName, secondDescriptor.ComponentType.FullName); + Assert.Equal(1, secondDescriptor.Sequence); + } + [Fact] public void CanParseMultipleMarkers() { @@ -517,4 +586,12 @@ private class DynamicallyAddedComponent : IComponent public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); } + + private class GenericTestComponent : IComponent + { + [Parameter] public T Value { get; set; } + + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + } }