Eliminate ElementRef's static incrementing ID for remote rendering cases

... because it's important not to disclose cross-user state, such as the number of IDs that have been assigned. Plus we don't want to run out of unique IDs, which we could if it's limited by the range of an 'int'.
This commit is contained in:
Steve Sanderson 2018-07-09 13:36:03 +01:00
parent c2f3128ef9
commit 767a2373c8
10 changed files with 64 additions and 32 deletions

View File

@ -153,7 +153,7 @@ export class BrowserRenderer {
return this.insertFrameRange(batch, componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame));
case FrameType.elementReferenceCapture:
if (parent instanceof Element) {
applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame));
applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame)!);
return 0; // A "capture" is a child in the diff, but has no node in the DOM
} else {
throw new Error('Reference capture frames can only be children of element frames.');

View File

@ -1,20 +1,20 @@
export function applyCaptureIdToElement(element: Element, referenceCaptureId: number) {
export function applyCaptureIdToElement(element: Element, referenceCaptureId: string) {
element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), '');
}
function getElementByCaptureId(referenceCaptureId: number) {
function getElementByCaptureId(referenceCaptureId: string) {
const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`;
return document.querySelector(selector);
}
function getCaptureIdAttributeName(referenceCaptureId: number) {
function getCaptureIdAttributeName(referenceCaptureId: string) {
return `_bl_${referenceCaptureId}`;
}
// Support receiving ElementRef instances as args in interop calls
const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs
DotNet.attachReviver((key, value) => {
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') {
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'string') {
return getElementByCaptureId(value[elementRefKey]);
} else {
return value;

View File

@ -43,7 +43,7 @@ export interface RenderTreeEditReader {
export interface RenderTreeFrameReader {
frameType(frame: RenderTreeFrame): FrameType;
subtreeLength(frame: RenderTreeFrame): number;
elementReferenceCaptureId(frame: RenderTreeFrame): number;
elementReferenceCaptureId(frame: RenderTreeFrame): string | null;
componentId(frame: RenderTreeFrame): number;
elementName(frame: RenderTreeFrame): string | null;
textContent(frame: RenderTreeFrame): string | null;

View File

@ -77,7 +77,7 @@ const frameReader = {
structLength: 28,
frameType: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 4) as FrameType,
subtreeLength: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
elementReferenceCaptureId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 8),
elementReferenceCaptureId: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
componentId: (frame: RenderTreeFrame) => platform.readInt32Field(frame as any, 12),
elementName: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),
textContent: (frame: RenderTreeFrame) => platform.readStringField(frame as any, 16),

View File

@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Blazor.Server.Circuits
WritePadding(_binaryWriter, 4);
break;
case RenderTreeFrameType.ElementReferenceCapture:
_binaryWriter.Write(frame.ElementReferenceCaptureId);
WriteString(frame.ElementReferenceCaptureId);
WritePadding(_binaryWriter, 8);
break;
case RenderTreeFrameType.Region:

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.JSInterop.Internal;
using System;
using System.Collections.Generic;
using System.Threading;
@ -12,25 +13,18 @@ namespace Microsoft.AspNetCore.Blazor
/// </summary>
public readonly struct ElementRef : ICustomJsonSerializer
{
// Static to ensure uniqueness even if there are multiple Renderer instances
// This would not be necessary if the JS-side code maintained a lookup from capureId to Element instances,
// but we're not doing that presently as it causes more work during disposal to remove those entries
// WARNING: Once we support server-side rendering, we should check if running on the server and avoid
// populating element reference capture IDs at all, because doing so could (a) eventually
// overflow the static int, and (b) disclose information to clients about how many other
// requests the server is handling, etc. In general, as part of implementing SSR, we need to
// audit the code it calls for any use of statics.
private static int _nextId = 0;
static long _nextIdForWebAssemblyOnly = 1;
internal int Id { get; }
// The Id is unique at least within the scope of a given user/circuit
internal string Id { get; }
private ElementRef(int id)
private ElementRef(string id)
{
Id = id;
}
internal static ElementRef CreateWithUniqueId()
=> new ElementRef(Interlocked.Increment(ref _nextId));
=> new ElementRef(CreateUniqueId());
object ICustomJsonSerializer.ToJsonPrimitive()
{
@ -39,5 +33,26 @@ namespace Microsoft.AspNetCore.Blazor
{ "_blazorElementRef", Id }
};
}
static string CreateUniqueId()
{
if (PlatformInfo.IsWebAssembly)
{
// On WebAssembly there's only one user, so it's fine to expose the number
// of IDs that have been assigned, and this is cheaper than creating a GUID.
// It's unfortunate that this still involves a heap allocation. If that becomes
// a problem we could extend RenderTreeFrame to have both "string" and "long"
// fields for ElementRefCaptureId, of which only one would be in use depending
// on the platform.
var id = Interlocked.Increment(ref _nextIdForWebAssemblyOnly);
return id.ToString();
}
else
{
// For remote rendering, it's important not to disclose any cross-user state,
// such as the number of IDs that have been assigned.
return Guid.NewGuid().ToString("D");
}
}
}
}

View File

@ -0,0 +1,17 @@
// 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.Runtime.InteropServices;
namespace Microsoft.AspNetCore.Blazor
{
internal static class PlatformInfo
{
public static bool IsWebAssembly { get; }
static PlatformInfo()
{
IsWebAssembly = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"));
}
}
}

View File

@ -137,13 +137,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
/// gets the ID of the reference capture. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(8)] public readonly int ElementReferenceCaptureId;
[FieldOffset(16)] public readonly string ElementReferenceCaptureId;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
/// gets the action that writes the reference to its target. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly Action<ElementRef> ElementReferenceCaptureAction;
[FieldOffset(24)] public readonly Action<ElementRef> ElementReferenceCaptureAction;
// --------------------------------------------------------------------------------
// RenderTreeFrameType.ComponentReferenceCapture
@ -227,7 +227,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
RegionSubtreeLength = regionSubtreeLength;
}
private RenderTreeFrame(int sequence, Action<ElementRef> elementReferenceCaptureAction, int elementReferenceCaptureId)
private RenderTreeFrame(int sequence, Action<ElementRef> elementReferenceCaptureAction, string elementReferenceCaptureId)
: this()
{
FrameType = RenderTreeFrameType.ElementReferenceCapture;
@ -264,7 +264,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
=> new RenderTreeFrame(sequence, regionSubtreeLength: 0);
internal static RenderTreeFrame ElementReferenceCapture(int sequence, Action<ElementRef> elementReferenceCaptureAction)
=> new RenderTreeFrame(sequence, elementReferenceCaptureAction: elementReferenceCaptureAction, elementReferenceCaptureId: 0);
=> new RenderTreeFrame(sequence, elementReferenceCaptureAction: elementReferenceCaptureAction, elementReferenceCaptureId: null);
internal static RenderTreeFrame ComponentReferenceCapture(int sequence, Action<object> componentReferenceCaptureAction, int parentFrameIndex)
=> new RenderTreeFrame(sequence, componentReferenceCaptureAction: componentReferenceCaptureAction, parentFrameIndex: parentFrameIndex);
@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength)
=> new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength);
internal RenderTreeFrame WithElementReferenceCaptureId(int elementReferenceCaptureId)
internal RenderTreeFrame WithElementReferenceCaptureId(string elementReferenceCaptureId)
=> new RenderTreeFrame(Sequence, ElementReferenceCaptureAction, elementReferenceCaptureId);
/// <inheritdoc />

View File

@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// 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 Microsoft.AspNetCore.Blazor.Components;
@ -1342,9 +1342,9 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Act
var (diff, referenceFrames) = GetSingleUpdatedComponent();
// Assert: Distinct nonzero IDs
Assert.NotEqual(0, ref1.Id);
Assert.NotEqual(0, ref2.Id);
// Assert: Distinct nonnull IDs
Assert.NotNull(ref1.Id);
Assert.NotNull(ref2.Id);
Assert.NotEqual(ref1.Id, ref2.Id);
// Assert: Also specified in diff
@ -1388,7 +1388,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
// Note: We're not preserving the ReferenceCaptureId on the actual RenderTreeFrames in the same
// way we do for event handler IDs, simply because there's no need to do so. We only do
// anything with ReferenceCaptureId when frames are first inserted into the document.
Assert.NotEqual(0, ref1.Id);
Assert.NotNull(ref1.Id);
Assert.Equal(1, refWriteCount);
Assert.Empty(diff.Edits);
Assert.Empty(referenceFrames);

View File

@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeFrame.Element(128, "Some element")
.WithElementSubtreeLength(1234),
RenderTreeFrame.ElementReferenceCapture(129, value => { })
.WithElementReferenceCaptureId(12121),
.WithElementReferenceCaptureId("my unique ID"),
RenderTreeFrame.Region(130)
.WithRegionSubtreeLength(1234),
RenderTreeFrame.Text(131, "Some text"),
@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
RenderTreeFrameType.Component, 5678, 2000, 0,
RenderTreeFrameType.ComponentReferenceCapture, 0, 0, 0,
RenderTreeFrameType.Element, 1234, "Some element", 0,
RenderTreeFrameType.ElementReferenceCapture, 12121, 0, 0,
RenderTreeFrameType.ElementReferenceCapture, "my unique ID", 0, 0,
RenderTreeFrameType.Region, 1234, 0, 0,
RenderTreeFrameType.Text, "Some text", 0, 0
);