Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NuGet.config
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
<!-- Begin: Package sources from dotnet-extensions -->
<!-- End: Package sources from dotnet-extensions -->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-3879076" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-runtime-3879076d/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-runtime -->
<!-- Begin: Package sources from dotnet-efcore -->
<add key="darc-int-dotnet-efcore-e3050de" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-efcore-e3050de1/nuget/v3/index.json" />
<!-- End: Package sources from dotnet-efcore -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
<add key="dotnet-eng" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-eng/nuget/v3/index.json" />
Expand All @@ -30,8 +32,10 @@
<clear />
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
<!-- Begin: Package sources from dotnet-efcore -->
<add key="darc-int-dotnet-efcore-e3050de" value="true" />
<!-- End: Package sources from dotnet-efcore -->
<!-- Begin: Package sources from dotnet-runtime -->
<add key="darc-int-dotnet-runtime-3879076" value="true" />
<!-- End: Package sources from dotnet-runtime -->
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
</disabledPackageSources>
Expand Down
320 changes: 160 additions & 160 deletions eng/Version.Details.xml

Large diffs are not rendered by default.

160 changes: 80 additions & 80 deletions eng/Versions.props

Large diffs are not rendered by default.

21 changes: 19 additions & 2 deletions src/Components/Web/src/JSComponents/JSComponentInterop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ static JSComponentInterop()
}

private const int MaxParameters = 100;
private readonly HashSet<int> _jsComponentIds = new();
private WebRenderer? _renderer;

internal JSComponentConfigurationStore Configuration { get; }
Expand Down Expand Up @@ -70,7 +71,9 @@ protected internal virtual int AddRootComponent(string identifier, string domEle
throw new ArgumentException($"There is no registered JS component with identifier '{identifier}'.");
}

return Renderer.AddRootComponent(componentType, domElementSelector);
var componentId = Renderer.AddRootComponent(componentType, domElementSelector);
_jsComponentIds.Add(componentId);
return componentId;
}

/// <summary>
Expand All @@ -80,6 +83,8 @@ protected internal virtual int AddRootComponent(string identifier, string domEle
Justification = "OpenComponent already has the right set of attributes")]
protected internal void SetRootComponentParameters(int componentId, int parameterCount, JsonElement parametersJson, JsonSerializerOptions jsonOptions)
{
EnsureValidJSComponentId(componentId);

// In case the client misreports the number of parameters, impose bounds so we know the amount
// of work done is limited to a fixed, low amount.
if (parameterCount < 0 || parameterCount > MaxParameters)
Expand Down Expand Up @@ -153,7 +158,11 @@ protected internal void SetRootComponentParameters(int componentId, int paramete
/// For framework use only.
/// </summary>
protected internal virtual void RemoveRootComponent(int componentId)
=> Renderer.RemoveRootComponent(componentId);
{
EnsureValidJSComponentId(componentId);
Renderer.RemoveRootComponent(componentId);
_jsComponentIds.Remove(componentId);
}

internal static ParameterTypeCache GetComponentParameters(Type componentType)
=> ParameterTypeCaches.GetOrAdd(componentType, static type => new ParameterTypeCache(type));
Expand Down Expand Up @@ -192,6 +201,14 @@ private static bool TryGetComponentParameterInfo(Type componentType, string para
return cacheForComponent.ParameterInfoByName.TryGetValue(parameterName, out parameterInfo);
}

private void EnsureValidJSComponentId(int componentId)
{
if (!_jsComponentIds.Contains(componentId))
{
throw new InvalidOperationException($"Root component with ID '{componentId}' was not added by JavaScript.");
}
}

internal readonly struct ParameterTypeCache
{
public readonly Dictionary<string, ParameterInfo> ParameterInfoByName;
Expand Down
128 changes: 128 additions & 0 deletions src/Components/Web/test/JSComponents/JSComponentInteropTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Web.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.JSInterop;
using Moq;

namespace Microsoft.AspNetCore.Components.Web;

public class JSComponentInteropTest
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);

[Fact]
public async Task SetRootComponentParameters_RejectsComponentNotAddedByJavaScript()
{
var (renderer, interop) = CreateRenderer();
var componentId = await renderer.Dispatcher.InvokeAsync(
() => renderer.AddRootComponent(typeof(TestComponent), "server-rendered"));
var parameters = JsonDocument.Parse("""{"value":"updated"}""").RootElement;

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => renderer.Dispatcher.InvokeAsync(
() => interop.SetRootComponentParameters(componentId, 1, parameters, JsonOptions)));

Assert.Equal($"Root component with ID '{componentId}' was not added by JavaScript.", exception.Message);
}

[Fact]
public async Task SetRootComponentParameters_AllowsComponentAddedByJavaScript()
{
var (renderer, interop) = CreateRenderer();
var componentId = await renderer.Dispatcher.InvokeAsync(
() => interop.AddRootComponent("test-component", "js-rendered"));
var parameters = JsonDocument.Parse("""{"value":"updated"}""").RootElement;

await renderer.Dispatcher.InvokeAsync(
() => interop.SetRootComponentParameters(componentId, 1, parameters, JsonOptions));

Assert.Equal("updated", renderer.GetComponent<TestComponent>(componentId).Value);
}

[Fact]
public async Task RemoveRootComponent_RejectsComponentNotAddedByJavaScript()
{
var (renderer, interop) = CreateRenderer();
var componentId = await renderer.Dispatcher.InvokeAsync(
() => renderer.AddRootComponent(typeof(TestComponent), "server-rendered"));

var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => renderer.Dispatcher.InvokeAsync(() => interop.RemoveRootComponent(componentId)));

Assert.Equal($"Root component with ID '{componentId}' was not added by JavaScript.", exception.Message);
}

[Fact]
public async Task RemoveRootComponent_AllowsComponentAddedByJavaScript()
{
var (renderer, interop) = CreateRenderer();
var componentId = await renderer.Dispatcher.InvokeAsync(
() => interop.AddRootComponent("test-component", "js-rendered"));

await renderer.Dispatcher.InvokeAsync(() => interop.RemoveRootComponent(componentId));

Assert.False(renderer.HasComponent(componentId));
}

private static (TestWebRenderer Renderer, JSComponentInterop Interop) CreateRenderer()
{
var configuration = new JSComponentConfigurationStore();
configuration.Add(typeof(TestComponent), "test-component");
var interop = new JSComponentInterop(configuration);
var services = new ServiceCollection()
.AddSingleton(Mock.Of<IJSRuntime>())
.BuildServiceProvider();
return (new TestWebRenderer(services, interop), interop);
}

private sealed class TestWebRenderer : WebRenderer
{
public TestWebRenderer(IServiceProvider services, JSComponentInterop interop)
: base(services, NullLoggerFactory.Instance, JsonOptions, interop)
{
}

public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault();

public new int AddRootComponent(Type componentType, string selector)
=> base.AddRootComponent(componentType, selector);

public TComponent GetComponent<TComponent>(int componentId)
where TComponent : IComponent
=> (TComponent)GetComponentState(componentId).Component;

public bool HasComponent(int componentId)
{
try
{
_ = GetComponentState(componentId);
return true;
}
catch (ArgumentException)
{
return false;
}
}

protected override void AttachRootComponentToBrowser(int componentId, string domElementSelector)
{
}

protected override void HandleException(Exception exception)
=> throw exception;

protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
=> Task.CompletedTask;
}

private sealed class TestComponent : ComponentBase
{
[Parameter]
public string Value { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Components/test/E2ETest/Tests/JSRootComponentsTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,18 @@ public void CanUpdateParameters()
Browser.Equal("5", () => clickCount.Text);
}

[Fact]
public void CannotUpdateParametersOnNonJSRootComponent()
{
var parameterValue = Browser.FindElement(By.Id("non-js-root-parameter"));

app.FindElement(By.Id("set-non-js-root-parameters")).Click();

Browser.Contains("Error setting parameters on non-JS root", () => app.FindElement(By.Id("message")).Text);
Browser.Equal("Initial non-JS root value", () => parameterValue.Text);
AssertGlobalErrorState(false);
}

[Fact]
public void CanSupplyComplexParameters()
{
Expand Down
4 changes: 4 additions & 0 deletions src/Components/test/testassets/BasicTestApp/Index.razor
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@using Microsoft.AspNetCore.Components.Rendering
@using System.Web
@inject NavigationManager NavigationManager
<p id="non-js-root-parameter">@NonJSRootParameter</p>
<div id="test-selector">
Select test:
<select id="test-selector-select" @bind=SelectedComponentTypeName>
Expand Down Expand Up @@ -134,6 +135,9 @@
</app>

@code {
[Parameter]
public string NonJSRootParameter { get; set; } = "Initial non-JS root value";

string SelectedComponentTypeName { get; set; } = "none";

Type SelectedComponentType
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<button id="add-root-component-inside-blazor" onclick="addRootComponent(true)">Add new root component inside Blazor UI</button>
<button id="remove-root-component" onclick="removeRootComponent()">Remove root component</button>
<button id="show-initializer-call-log" onclick="showInitializerCallLog()">Show initializer calls</button>
<button id="set-non-js-root-parameters" onclick="setNonJSRootParameters()">Set non-JS root parameters</button>
</p>

<p>
Expand Down Expand Up @@ -160,6 +161,15 @@
setMessage(JSON.stringify(callLog));
}

async function setNonJSRootParameters() {
const error = await myJsRootComponentInitializers.setRootComponentParameters(
0,
{ nonJSRootParameter: 'Updated non-JS root value' });
setMessage(error
? `Error setting parameters on non-JS root: ${error}`
: 'Unexpectedly updated parameters on non-JS root');
}

function setMessage(message) {
document.getElementById('message').textContent = message;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@

<script>
(function () {
myJsRootComponentInitializers.captureRendererInterop(Blazor);
if (location.hash.indexOf('initializer') !== -1) {
const element = document.createElement('div');
element.id = 'initializers-content';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
(function () {
const callLog = [];
const state = window.__jsRootComponentInitializerState ??= {};

window.myJsRootComponentInitializers = {
captureRendererInterop(blazor) {
const originalAttachWebRendererInterop = blazor._internal.attachWebRendererInterop;
blazor._internal.attachWebRendererInterop = function (rendererId, interopMethods, ...configuration) {
state.rendererInterop = interopMethods;
return originalAttachWebRendererInterop.call(this, rendererId, interopMethods, ...configuration);
};
},

testInitializer: function (name, parameters) {
// Just keep track of the info we received so the E2E test can assert it was correct
callLog.push({ name: name, parameters: parameters });
},

getCallLog() {
return callLog;
},

async setRootComponentParameters(componentId, parameters) {
try {
await state.rendererInterop.invokeMethodAsync(
'SetRootComponentParameters',
componentId,
Object.keys(parameters).length,
parameters);
return null;
} catch (error) {
return error.toString();
}
}
};
})();
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@

<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
myJsRootComponentInitializers.captureRendererInterop(Blazor);
Blazor.start({
reconnectionOptions: {
// It's easier to test the reconnection logic if we wait a bit
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Grpc.JsonTranscoding.Internal.CallHandlers;

internal sealed class CallHandlerDescriptorInfo
{
internal readonly int MaxPathDescriptorsCacheCount;

public CallHandlerDescriptorInfo(
FieldDescriptor? responseBodyDescriptor,
MessageDescriptor? bodyDescriptor,
Expand All @@ -26,12 +28,12 @@ public CallHandlerDescriptorInfo(
RouteAdapter = routeAdapter;
PathDescriptorsCache = new ConcurrentDictionary<string, List<FieldDescriptor>>();

var jsonPaths = new HashSet<string>(StringComparer.Ordinal);
foreach (var routeParameter in routeParameterDescriptors.Values)
MaxPathDescriptorsCacheCount = AppContext.GetData("Microsoft.AspNetCore.Grpc.JsonTranscoding.MaxPathDescriptorsCacheCount") switch
{
jsonPaths.Add(routeParameter.JsonPath);
}
RouteParameterJsonPaths = jsonPaths;
int count when count > 0 => count,
string countStr when int.TryParse(countStr, out var parsed) && parsed > 0 => parsed,
_ => 1000,
};
}

public FieldDescriptor? ResponseBodyDescriptor { get; }
Expand All @@ -41,6 +43,15 @@ public CallHandlerDescriptorInfo(
public FieldDescriptor? BodyFieldDescriptor { get; }
public Dictionary<string, RouteParameter> RouteParameterDescriptors { get; }
public JsonTranscodingRouteAdapter RouteAdapter { get; }
public HashSet<string> RouteParameterJsonPaths { get; }
public ConcurrentDictionary<string, List<FieldDescriptor>> PathDescriptorsCache { get; }

public bool TryAddPathDescriptors(string path, List<FieldDescriptor> pathDescriptors)
{
if (PathDescriptorsCache.Count >= MaxPathDescriptorsCacheCount)
{
return false;
}

return PathDescriptorsCache.TryAdd(path, pathDescriptors);
}
}
Loading
Loading