Skip to content

Commit 0ecb174

Browse files
committed
Merge in 'release/8.0' changes
2 parents a1d1ced + 4373734 commit 0ecb174

File tree

6 files changed

+217
-23
lines changed

6 files changed

+217
-23
lines changed

src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
using System.Diagnostics;
66
using System.Diagnostics.CodeAnalysis;
77
using System.Globalization;
8-
using System.Security.Cryptography;
9-
using System.Text;
108
using Microsoft.AspNetCore.Builder;
119
using Microsoft.AspNetCore.Components.Rendering;
1210
using Microsoft.AspNetCore.Components.Web;
@@ -192,7 +190,7 @@ public ComponentMarker ToMarker(HttpContext httpContext, int sequence, object? c
192190

193191
private ComponentMarkerKey GenerateMarkerKey(int sequence, object? componentKey)
194192
{
195-
var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, ComputeComponentTypeNameHash);
193+
var componentTypeNameHash = _componentTypeNameHashCache.GetOrAdd(_componentType, TypeNameHash.Compute);
196194
var sequenceString = sequence.ToString(CultureInfo.InvariantCulture);
197195

198196
var locationHash = $"{componentTypeNameHash}:{sequenceString}";
@@ -204,24 +202,4 @@ private ComponentMarkerKey GenerateMarkerKey(int sequence, object? componentKey)
204202
FormattedComponentKey = formattedComponentKey,
205203
};
206204
}
207-
208-
private static string ComputeComponentTypeNameHash(Type componentType)
209-
{
210-
if (componentType.FullName is not { } typeName)
211-
{
212-
throw new InvalidOperationException($"An invalid component type was used in {nameof(SSRRenderModeBoundary)}.");
213-
}
214-
215-
var typeNameLength = typeName.Length;
216-
var typeNameBytes = typeNameLength < 1024
217-
? stackalloc byte[typeNameLength]
218-
: new byte[typeNameLength];
219-
220-
Encoding.UTF8.GetBytes(typeName, typeNameBytes);
221-
222-
Span<byte> typeNameHashBytes = stackalloc byte[SHA1.HashSizeInBytes];
223-
SHA1.HashData(typeNameBytes, typeNameHashBytes);
224-
225-
return Convert.ToHexString(typeNameHashBytes);
226-
}
227205
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Security.Cryptography;
5+
using System.Text;
6+
7+
namespace Microsoft.AspNetCore.Components.Endpoints;
8+
9+
// Internal for testing.
10+
internal class TypeNameHash
11+
{
12+
public const int MaxStackBufferSize = 1024;
13+
14+
public static string Compute(Type type)
15+
{
16+
if (type.FullName is not { } typeName)
17+
{
18+
throw new InvalidOperationException($"Cannot compute a hash for a type without a {nameof(Type.FullName)}.");
19+
}
20+
21+
Span<byte> typeNameBytes = stackalloc byte[MaxStackBufferSize];
22+
23+
if (!Encoding.UTF8.TryGetBytes(typeName, typeNameBytes, out var written))
24+
{
25+
typeNameBytes = Encoding.UTF8.GetBytes(typeName);
26+
written = typeNameBytes.Length;
27+
}
28+
29+
Span<byte> typeNameHashBytes = stackalloc byte[SHA256.HashSizeInBytes];
30+
SHA256.HashData(typeNameBytes[..written], typeNameHashBytes);
31+
32+
return Convert.ToHexString(typeNameHashBytes);
33+
}
34+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Security.Cryptography;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints;
7+
8+
public class TypeNameHashTest
9+
{
10+
// In these tests, we're mostly interested in checking that the hash function succeeds
11+
// for any type with a valid name. We'll also do some basic sanity checking by ensuring
12+
// that the string representation of the hash has the expected length.
13+
14+
// We currently use a hex-encoded SHA256 hash, so there should be two characters per byte
15+
// of encoded data.
16+
private const int ExpectedHashLength = SHA256.HashSizeInBytes * 2;
17+
18+
[Fact]
19+
public void CanComputeHashForTypeWithBasicName()
20+
{
21+
// Act
22+
var hash = TypeNameHash.Compute(typeof(ClassWithBasicName));
23+
24+
// Assert
25+
Assert.Equal(ExpectedHashLength, hash.Length);
26+
}
27+
28+
[Fact]
29+
public void CanComputeHashForTypeWithMultibyteCharacters()
30+
{
31+
// Act
32+
var hash = TypeNameHash.Compute(typeof(ClássWïthMûltibyteÇharacters));
33+
34+
// Assert
35+
Assert.Equal(ExpectedHashLength, hash.Length);
36+
}
37+
38+
[Fact]
39+
public void CanComputeHashForAnonymousType()
40+
{
41+
// Arrange
42+
var type = new { Foo = "bar" }.GetType();
43+
44+
// Act
45+
var hash = TypeNameHash.Compute(type);
46+
47+
// Assert
48+
Assert.Equal(ExpectedHashLength, hash.Length);
49+
}
50+
51+
[Fact]
52+
public void CanComputeHashForTypeWithNameLongerThanMaxStackBufferSize()
53+
{
54+
// Arrange
55+
// We need to use a type with a long name, so we'll use a large tuple.
56+
// We have an assert later in this test to sanity check that the type
57+
// name is indeed longer than the max stack buffer size.
58+
var type = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12).GetType();
59+
60+
// Act
61+
var hash = TypeNameHash.Compute(type);
62+
63+
// Assert
64+
Assert.True(type.FullName.Length > TypeNameHash.MaxStackBufferSize);
65+
Assert.Equal(ExpectedHashLength, hash.Length);
66+
}
67+
68+
[Fact]
69+
public void ThrowsIfTypeHasNoName()
70+
{
71+
// Arrange
72+
var type = typeof(Nullable<>).GetGenericArguments()[0];
73+
74+
// Act/Assert
75+
var ex = Assert.Throws<InvalidOperationException>(() => TypeNameHash.Compute(type));
76+
Assert.Equal($"Cannot compute a hash for a type without a {nameof(Type.FullName)}.", ex.Message);
77+
}
78+
79+
class ClassWithBasicName;
80+
class ClássWïthMûltibyteÇharacters;
81+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Components.TestServer.RazorComponents;
5+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
6+
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
7+
using Microsoft.AspNetCore.E2ETesting;
8+
using OpenQA.Selenium;
9+
using TestServer;
10+
using Xunit.Abstractions;
11+
12+
namespace Microsoft.AspNetCore.Components.E2ETests.ServerRenderingTests;
13+
14+
public class MultibyteComponentTypeNameTest : ServerTestBase<BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>>>
15+
{
16+
public MultibyteComponentTypeNameTest(
17+
BrowserFixture browserFixture,
18+
BasicTestAppServerSiteFixture<RazorComponentEndpointsStartup<App>> serverFixture,
19+
ITestOutputHelper output)
20+
: base(browserFixture, serverFixture, output)
21+
{
22+
}
23+
24+
[Theory]
25+
[InlineData("server")]
26+
[InlineData("webassembly")]
27+
public void CanRenderInteractiveComponentsWithMultibyteName(string renderMode)
28+
{
29+
Navigate($"{ServerPathBase}/multibyte-character-component/{renderMode}");
30+
31+
Browser.Equal("True", () => Browser.FindElement(By.ClassName("is-interactive")).Text);
32+
Browser.Equal("0", () => Browser.FindElement(By.ClassName("count")).Text);
33+
34+
Browser.FindElement(By.ClassName("increment")).Click();
35+
36+
Browser.Equal("1", () => Browser.FindElement(By.ClassName("count")).Text);
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@page "/multibyte-character-component/{renderModeString?}"
2+
@using TestContentPackage
3+
4+
<h1>Page rendering component with multibyte type name</h1>
5+
6+
@if (_renderMode is null)
7+
{
8+
<p>
9+
<b>Warning:</b> Render mode should be specified as a route parameter and have the value 'server' or 'webassembly'.
10+
</p>
11+
12+
<p>
13+
Defaulting to a null render mode.
14+
</p>
15+
}
16+
17+
<MûltibyteÇharacterCompoñent @rendermode="_renderMode" />
18+
19+
@code {
20+
private IComponentRenderMode? _renderMode;
21+
22+
[Parameter]
23+
public string? RenderModeString { get; set; }
24+
25+
protected override void OnInitialized()
26+
{
27+
if (string.Equals("server", RenderModeString, StringComparison.OrdinalIgnoreCase))
28+
{
29+
_renderMode = RenderMode.InteractiveServer;
30+
}
31+
else if (string.Equals("webassembly", RenderModeString, StringComparison.OrdinalIgnoreCase))
32+
{
33+
_renderMode = RenderMode.InteractiveWebAssembly;
34+
}
35+
}
36+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<p>
2+
<button class="increment" type="button" @onclick="IncrementCount">Click me</button>
3+
Count: <span class="count">@_count</span>
4+
</p>
5+
6+
<p>
7+
Is interactive: <span class="is-interactive">@_isInteractive</span>
8+
</p>
9+
10+
@code {
11+
private int _count = 0;
12+
private bool _isInteractive;
13+
14+
private void IncrementCount()
15+
{
16+
_count++;
17+
}
18+
19+
protected override void OnAfterRender(bool firstRender)
20+
{
21+
if (firstRender)
22+
{
23+
_isInteractive = true;
24+
StateHasChanged();
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)